From 63371a8fb6f8f0ec26bbb4a0e7d24f3b5ddc22d8 Mon Sep 17 00:00:00 2001 From: Herman Slatman Date: Thu, 9 Dec 2021 09:36:03 +0100 Subject: [PATCH] Add additional tests for ACME EAB Admin --- acme/db.go | 12 +- authority/admin/api/acme.go | 27 +- authority/admin/api/acme_test.go | 750 +++++++++++++++++++++++++++++++ 3 files changed, 775 insertions(+), 14 deletions(-) diff --git a/acme/db.go b/acme/db.go index 73029231..736df5d4 100644 --- a/acme/db.go +++ b/acme/db.go @@ -19,7 +19,7 @@ type DB interface { GetAccountByKeyID(ctx context.Context, kid string) (*Account, error) UpdateAccount(ctx context.Context, acc *Account) error - CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) GetExternalAccountKey(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) GetExternalAccountKeys(ctx context.Context, provisionerName, cursor string, limit int) ([]*ExternalAccountKey, string, error) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) @@ -54,7 +54,7 @@ type MockDB struct { MockGetAccountByKeyID func(ctx context.Context, kid string) (*Account, error) MockUpdateAccount func(ctx context.Context, acc *Account) error - MockCreateExternalAccountKey func(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) + MockCreateExternalAccountKey func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) MockGetExternalAccountKey func(ctx context.Context, provisionerName, keyID string) (*ExternalAccountKey, error) MockGetExternalAccountKeys func(ctx context.Context, provisionerName string, cursor string, limit int) ([]*ExternalAccountKey, string, error) MockGetExternalAccountKeyByReference func(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) @@ -125,9 +125,9 @@ func (m *MockDB) UpdateAccount(ctx context.Context, acc *Account) error { } // CreateExternalAccountKey mock -func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, name string) (*ExternalAccountKey, error) { +func (m *MockDB) CreateExternalAccountKey(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { if m.MockCreateExternalAccountKey != nil { - return m.MockCreateExternalAccountKey(ctx, provisionerName, name) + return m.MockCreateExternalAccountKey(ctx, provisionerName, reference) } else if m.MockError != nil { return nil, m.MockError } @@ -156,8 +156,8 @@ func (m *MockDB) GetExternalAccountKeys(ctx context.Context, provisionerName, cu // GetExternalAccountKeyByReference mock func (m *MockDB) GetExternalAccountKeyByReference(ctx context.Context, provisionerName, reference string) (*ExternalAccountKey, error) { - if m.MockGetExternalAccountKeys != nil { - return m.GetExternalAccountKeyByReference(ctx, provisionerName, reference) + if m.MockGetExternalAccountKeyByReference != nil { + return m.MockGetExternalAccountKeyByReference(ctx, provisionerName, reference) } else if m.MockError != nil { return nil, m.MockError } diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 88e76a09..39aeed49 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -2,6 +2,8 @@ package api import ( "context" + "errors" + "fmt" "net/http" "github.com/go-chi/chi" @@ -20,6 +22,9 @@ type CreateExternalAccountKeyRequest struct { // Validate validates a new ACME EAB Key request body. func (r *CreateExternalAccountKeyRequest) Validate() error { + if len(r.Reference) > 256 { // an arbitrary, but sensible (IMO), limit + return fmt.Errorf("reference length %d exceeds the maximum (256)", len(r.Reference)) + } return nil } @@ -85,7 +90,7 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques } if err := body.Validate(); err != nil { - api.WriteError(w, err) + api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, "error validating request body")) return } @@ -97,9 +102,9 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques k, err := h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) // retrieving an EAB key from DB results in an error if it doesn't exist, which is what we're looking for, // but other errors can also happen. Return early if that happens; continuing if it was acme.ErrNotFound. - shouldWriteError := err != nil && acme.ErrNotFound != err + shouldWriteError := err != nil && !errors.Is(err, acme.ErrNotFound) if shouldWriteError { - api.WriteError(w, err) + api.WriteError(w, admin.WrapErrorISE(err, "could not lookup external account key by reference")) return } // if a key was found, return HTTP 409 conflict @@ -114,7 +119,11 @@ func (h *Handler) CreateExternalAccountKey(w http.ResponseWriter, r *http.Reques eak, err := h.acmeDB.CreateExternalAccountKey(r.Context(), prov, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error creating ACME EAB key for provisioner %s and reference %s", prov, reference)) + msg := fmt.Sprintf("error creating ACME EAB key for provisioner '%s'", prov) + if reference != "" { + msg += fmt.Sprintf(" and reference '%s'", reference) + } + api.WriteError(w, admin.WrapErrorISE(err, msg)) return } @@ -134,7 +143,7 @@ func (h *Handler) DeleteExternalAccountKey(w http.ResponseWriter, r *http.Reques keyID := chi.URLParam(r, "id") if err := h.acmeDB.DeleteExternalAccountKey(r.Context(), prov, keyID); err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key %s", keyID)) + api.WriteError(w, admin.WrapErrorISE(err, "error deleting ACME EAB Key '%s'", keyID)) return } @@ -165,14 +174,16 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) if reference != "" { key, err = h.acmeDB.GetExternalAccountKeyByReference(r.Context(), prov, reference) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error getting external account key with reference %s", reference)) + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account key with reference '%s'", reference)) return } - keys = []*acme.ExternalAccountKey{key} + if key != nil { + keys = []*acme.ExternalAccountKey{key} + } } else { keys, nextCursor, err = h.acmeDB.GetExternalAccountKeys(r.Context(), prov, cursor, limit) if err != nil { - api.WriteError(w, admin.WrapErrorISE(err, "error getting external account keys")) + api.WriteError(w, admin.WrapErrorISE(err, "error retrieving external account keys")) return } } diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 84e8e9f5..39238852 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -8,16 +8,33 @@ import ( "io" "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/go-chi/chi" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/smallstep/assert" + "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/linkedca" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) +func readProtoJSON(r io.ReadCloser, m proto.Message) error { + defer r.Close() + data, err := io.ReadAll(r) + if err != nil { + return err + } + return protojson.Unmarshal(data, m) +} + func TestHandler_requireEABEnabled(t *testing.T) { type test struct { ctx context.Context @@ -414,3 +431,736 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) { }) } } + +func TestHandler_CreateExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + body []byte + statusCode int + eak *linkedca.EABKey + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + body := []byte("{!?}") + return test{ + ctx: ctx, + body: body, + statusCode: 400, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error reading request body: error decoding json: invalid character '!' looking for beginning of object key string", + }, + } + }, + "fail/validate": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: strings.Repeat("A", 257), + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: ctx, + body: body, + statusCode: 400, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "error validating request body: reference length 257 exceeds the maximum (256)", + }, + } + }, + "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "could not lookup external account key by reference: force", + }, + } + }, + "fail/reference-conflict-409": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + past := time.Now().Add(-24 * time.Hour) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: past, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 409, + eak: nil, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 409, + Detail: "bad request", + Message: "an ACME EAB key for provisioner provName with reference an-external-key-reference already exists", + }, + } + }, + "fail/acmeDB.CreateExternalAccountKey-no-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error creating ACME EAB key for provisioner 'provName': force", + }, + } + }, + "fail/acmeDB.CreateExternalAccountKey-with-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict + }, + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error creating ACME EAB key for provisioner 'provName' and reference 'an-external-key-reference': force", + }, + } + }, + "ok/no-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + now := time.Now() + db := &acme.MockDB{ + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 201, + eak: &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provName", + Reference: "", + HmacKey: []byte{1, 3, 3, 7}, + }, + } + }, + "ok/with-reference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + req := CreateExternalAccountKeyRequest{ + Reference: "an-external-key-reference", + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + now := time.Now() + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict + }, + MockCreateExternalAccountKey: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, nil + }, + } + return test{ + ctx: ctx, + db: db, + body: body, + statusCode: 201, + eak: &linkedca.EABKey{ + Id: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + HmacKey: []byte{1, 3, 3, 7}, + }, + } + }, + } + + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := httptest.NewRequest("POST", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + eabKey := &linkedca.EABKey{} + err := readProtoJSON(res.Body, eabKey) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{})} + if !cmp.Equal(tc.eak, eabKey, opts...) { + t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.eak, eabKey, opts...)) + } + } + }) + } +} + +func TestHandler_DeleteExternalAccountKey(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/acmeDB.DeleteExternalAccountKey": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("id", "keyID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "keyID", keyID) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + db: db, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error deleting ACME EAB Key 'keyID': force", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("id", "keyID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockDeleteExternalAccountKey: func(ctx context.Context, provisionerName, keyID string) error { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "keyID", keyID) + return nil + }, + } + return test{ + ctx: ctx, + db: db, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteExternalAccountKey(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := DeleteResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + assert.Equals(t, "ok", response.Status) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } + }) + } +} + +func TestHandler_GetExternalAccountKeys(t *testing.T) { + type test struct { + ctx context.Context + db acme.DB + statusCode int + req *http.Request + resp GetExternalAccountKeysResponse + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + return test{ + ctx: ctx, + statusCode: 400, + req: req, + err: &admin.Error{ + Status: 400, + Type: admin.ErrorBadRequestType.String(), + Detail: "bad request", + Message: "error parsing cursor and limit from query params: limit 'A' is not an integer: strconv.Atoi: parsing \"A\": invalid syntax", + }, + } + }, + "fail/acmeDB.GetExternalAccountKeyByReference": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("ref", "an-external-key-reference") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + statusCode: 500, + req: req, + db: db, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving external account key with reference 'an-external-key-reference': force", + }, + } + }, + "fail/acmeDB.GetExternalAccountKeys": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: ctx, + statusCode: 500, + req: req, + db: db, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving external account keys: force", + }, + } + }, + "ok/reference-not-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("ref", "an-external-key-reference") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return nil, nil // returning nil; no key found + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{}, + NextCursor: "", + }, + db: db, + err: nil, + } + }, + "ok/reference-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + chiCtx.URLParams.Add("ref", "an-external-key-reference") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + db := &acme.MockDB{ + MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerName, reference string) (*acme.ExternalAccountKey, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "an-external-key-reference", reference) + return &acme.ExternalAccountKey{ + ID: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + CreatedAt: createdAt, + }, nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID", + Provisioner: "provName", + Reference: "an-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + }, + NextCursor: "", + }, + db: db, + err: nil, + } + }, + "ok/multiple-keys": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + boundAtSet := time.Now().Add(-12 * time.Hour) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return []*acme.ExternalAccountKey{ + { + ID: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + { + ID: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt.Add(1 * time.Hour), + }, + { + ID: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + BoundAt: boundAtSet, + AccountID: "accountID", + }, + }, "nextCursorValue", nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAtSet), + Account: "accountID", + }, + }, + NextCursor: "nextCursorValue", + }, + db: db, + err: nil, + } + }, + "ok/multiple-keys-with-cursor-and-limit": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("prov", "provName") + req := httptest.NewRequest("GET", "/foo?cursor=eakID1&limit=10", nil) + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now().Add(-24 * time.Hour) + var boundAt time.Time + boundAtSet := time.Now().Add(-12 * time.Hour) + db := &acme.MockDB{ + MockGetExternalAccountKeys: func(ctx context.Context, provisionerName, cursor string, limit int) ([]*acme.ExternalAccountKey, string, error) { + assert.Equals(t, "provName", provisionerName) + assert.Equals(t, "eakID1", cursor) + assert.Equals(t, 10, limit) + return []*acme.ExternalAccountKey{ + { + ID: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + }, + { + ID: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt.Add(1 * time.Hour), + }, + { + ID: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: createdAt, + BoundAt: boundAtSet, + AccountID: "accountID", + }, + }, "eakID4", nil + }, + } + return test{ + ctx: ctx, + statusCode: 200, + req: req, + resp: GetExternalAccountKeysResponse{ + EAKs: []*linkedca.EABKey{ + { + Id: "eakID1", + Provisioner: "provName", + Reference: "some-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID2", + Provisioner: "provName", + Reference: "some-other-external-key-reference", + CreatedAt: timestamppb.New(createdAt.Add(1 * time.Hour)), + BoundAt: timestamppb.New(boundAt), + }, + { + Id: "eakID3", + Provisioner: "provName", + Reference: "another-external-key-reference", + CreatedAt: timestamppb.New(createdAt), + BoundAt: timestamppb.New(boundAtSet), + Account: "accountID", + }, + }, + NextCursor: "eakID4", + }, + db: db, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + acmeDB: tc.db, + } + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetExternalAccountKeys(w, req) + res := w.Result() + assert.Equals(t, res.StatusCode, tc.statusCode) + + if res.StatusCode >= 400 { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + adminErr := admin.Error{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &adminErr)) + + assert.Equals(t, tc.err.Type, adminErr.Type) + assert.Equals(t, tc.err.Message, adminErr.Message) + assert.Equals(t, tc.err.StatusCode(), res.StatusCode) + assert.Equals(t, tc.err.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + } else { + body, err := io.ReadAll(res.Body) + res.Body.Close() + assert.FatalError(t, err) + + response := GetExternalAccountKeysResponse{} + assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &response)) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.EABKey{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("h.CreateExternalAccountKey diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + } + }) + } +}