forked from TrueCloudLab/certificates
Add additional tests for ACME EAB Admin
This commit is contained in:
parent
2215a05c28
commit
63371a8fb6
3 changed files with 775 additions and 14 deletions
12
acme/db.go
12
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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...))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue