smallstep-certificates/authority/admin/api/acme_test.go
Herman Slatman ef16febf40
Refactor ACME EAB queries
The ACME EAB keys are now also indexed by the provisioner. This
solves part of the issue in which too many EAB keys may be in
memory at a given time.
2022-01-07 16:59:55 +01:00

1099 lines
33 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"errors"
"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/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
db admin.DB
auth adminAuthority
next nextHTTP
err *admin.Error
statusCode int
}
var tests = map[string]func(t *testing.T) test{
"fail/h.provisionerHasEABEnabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return nil, errors.New("force")
},
}
err := admin.NewErrorISE("error loading provisioner provName: force")
err.Message = "error loading provisioner provName: force"
return test{
ctx: ctx,
auth: auth,
err: err,
statusCode: 500,
}
},
"ok/eab-disabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
RequireEab: false,
},
},
},
}, nil
},
}
err := admin.NewError(admin.ErrorBadRequestType, "ACME EAB not enabled for provisioner provName")
err.Message = "ACME EAB not enabled for provisioner provName"
return test{
ctx: ctx,
auth: auth,
db: db,
err: err,
statusCode: 400,
}
},
"ok/eab-enabled": func(t *testing.T) test {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("prov", "provName")
ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx)
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
RequireEab: true,
},
},
},
}, nil
},
}
return test{
ctx: ctx,
auth: auth,
db: db,
next: func(w http.ResponseWriter, r *http.Request) {
w.Write(nil) // mock response with status 200
},
statusCode: 200,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
h := &Handler{
db: tc.db,
auth: tc.auth,
acmeDB: nil,
}
req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup
req = req.WithContext(tc.ctx)
w := httptest.NewRecorder()
h.requireEABEnabled(tc.next)(w, req)
res := w.Result()
assert.Equals(t, tc.statusCode, res.StatusCode)
body, err := io.ReadAll(res.Body)
res.Body.Close()
assert.FatalError(t, err)
if res.StatusCode >= 400 {
err := admin.Error{}
assert.FatalError(t, json.Unmarshal(bytes.TrimSpace(body), &err))
assert.Equals(t, tc.err.Type, err.Type)
assert.Equals(t, tc.err.Message, err.Message)
assert.Equals(t, tc.err.StatusCode(), res.StatusCode)
assert.Equals(t, tc.err.Detail, err.Detail)
assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"])
return
}
})
}
}
func TestHandler_provisionerHasEABEnabled(t *testing.T) {
type test struct {
db admin.DB
auth adminAuthority
provisionerName string
want bool
err *admin.Error
}
var tests = map[string]func(t *testing.T) test{
"fail/auth.LoadProvisionerByName": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return nil, errors.New("force")
},
}
return test{
auth: auth,
provisionerName: "provName",
want: false,
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
}
},
"fail/db.GetProvisioner": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return nil, errors.New("force")
},
}
return test{
auth: auth,
db: db,
provisionerName: "provName",
want: false,
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
}
},
"fail/prov.GetDetails": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Details: nil,
}, nil
},
}
return test{
auth: auth,
db: db,
provisionerName: "provName",
want: false,
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
}
},
"fail/details.GetACME": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "provName", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "provName",
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: nil,
},
},
}, nil
},
}
return test{
auth: auth,
db: db,
provisionerName: "provName",
want: false,
err: admin.WrapErrorISE(errors.New("force"), "error loading provisioner provName"),
}
},
"ok/eab-disabled": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "eab-disabled", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "eab-disabled",
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
RequireEab: false,
},
},
},
}, nil
},
}
return test{
db: db,
auth: auth,
provisionerName: "eab-disabled",
want: false,
}
},
"ok/eab-enabled": func(t *testing.T) test {
auth := &mockAdminAuthority{
MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) {
assert.Equals(t, "eab-enabled", name)
return &provisioner.MockProvisioner{
MgetID: func() string {
return "provID"
},
}, nil
},
}
db := &admin.MockDB{
MockGetProvisioner: func(ctx context.Context, id string) (*linkedca.Provisioner, error) {
assert.Equals(t, "provID", id)
return &linkedca.Provisioner{
Id: "provID",
Name: "eab-enabled",
Details: &linkedca.ProvisionerDetails{
Data: &linkedca.ProvisionerDetails_ACME{
ACME: &linkedca.ACMEProvisioner{
RequireEab: true,
},
},
},
}, nil
},
}
return test{
db: db,
auth: auth,
provisionerName: "eab-enabled",
want: true,
}
},
}
for name, prep := range tests {
tc := prep(t)
t.Run(name, func(t *testing.T) {
h := &Handler{
db: tc.db,
auth: tc.auth,
acmeDB: nil,
}
got, prov, err := h.provisionerHasEABEnabled(context.TODO(), tc.provisionerName)
if (err != nil) != (tc.err != nil) {
t.Errorf("Handler.provisionerHasEABEnabled() error = %v, want err %v", err, tc.err)
return
}
if tc.err != nil {
assert.Type(t, &linkedca.Provisioner{}, prov)
assert.Type(t, &admin.Error{}, err)
adminError, _ := err.(*admin.Error)
assert.Equals(t, tc.err.Type, adminError.Type)
assert.Equals(t, tc.err.Status, adminError.Status)
assert.Equals(t, tc.err.StatusCode(), adminError.StatusCode())
assert.Equals(t, tc.err.Message, adminError.Message)
assert.Equals(t, tc.err.Detail, adminError.Detail)
return
}
if got != tc.want {
t.Errorf("Handler.provisionerHasEABEnabled() = %v, want %v", got, tc.want)
}
})
}
}
func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
type fields struct {
Reference string
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "fail/reference-too-long",
fields: fields{
Reference: strings.Repeat("A", 257),
},
wantErr: true,
},
{
name: "ok/empty-reference",
fields: fields{
Reference: "",
},
wantErr: false,
},
{
name: "ok",
fields: fields{
Reference: "my-eab-reference",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := &CreateExternalAccountKeyRequest{
Reference: tt.fields.Reference,
}
if err := r.Validate(); (err != nil) != tt.wantErr {
t.Errorf("CreateExternalAccountKeyRequest.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestHandler_CreateExternalAccountKey(t *testing.T) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference",
}
body, err := json.Marshal(req)
assert.FatalError(t, err)
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference",
}
body, err := json.Marshal(req)
assert.FatalError(t, err)
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference)
past := time.Now().Add(-24 * time.Hour)
return &acme.ExternalAccountKey{
ID: "eakID",
ProvisionerID: "provID",
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
Reference: "",
}
body, err := json.Marshal(req)
assert.FatalError(t, err)
db := &acme.MockDB{
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
Reference: "an-external-key-reference",
}
body, err := json.Marshal(req)
assert.FatalError(t, err)
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference)
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
},
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
req := CreateExternalAccountKeyRequest{
Reference: "",
}
body, err := json.Marshal(req)
assert.FatalError(t, err)
now := time.Now()
db := &acme.MockDB{
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "", reference)
return &acme.ExternalAccountKey{
ID: "eakID",
ProvisionerID: "provID",
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
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, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference)
return nil, acme.ErrNotFound // simulating not found; skipping 409 conflict
},
MockCreateExternalAccountKey: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference)
return &acme.ExternalAccountKey{
ID: "eakID",
ProvisionerID: "provID",
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, tc.statusCode, res.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"])
return
}
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) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockDeleteExternalAccountKey: func(ctx context.Context, provisionerID, keyID string) error {
assert.Equals(t, "provID", provisionerID)
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, tc.statusCode, res.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"])
return
}
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) {
prov := &linkedca.Provisioner{
Id: "provID",
Name: "provName",
}
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/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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockGetExternalAccountKeys: func(ctx context.Context, provisionerID string) ([]*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
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{},
},
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
createdAt := time.Now().Add(-24 * time.Hour)
var boundAt time.Time
db := &acme.MockDB{
MockGetExternalAccountKeyByReference: func(ctx context.Context, provisionerID, reference string) (*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
assert.Equals(t, "an-external-key-reference", reference)
return &acme.ExternalAccountKey{
ID: "eakID",
ProvisionerID: "provID",
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),
},
},
},
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)
ctx = context.WithValue(ctx, provisionerContextKey, prov)
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, provisionerID string) ([]*acme.ExternalAccountKey, error) {
assert.Equals(t, "provID", provisionerID)
return []*acme.ExternalAccountKey{
{
ID: "eakID1",
ProvisionerID: "provID",
Reference: "some-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt,
},
{
ID: "eakID2",
ProvisionerID: "provID",
Reference: "some-other-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt.Add(1 * time.Hour),
},
{
ID: "eakID3",
ProvisionerID: "provID",
Reference: "another-external-key-reference",
KeyBytes: []byte{1, 3, 3, 7},
CreatedAt: createdAt,
BoundAt: boundAtSet,
AccountID: "accountID",
},
}, 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",
},
},
},
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, tc.statusCode, res.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"])
return
}
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.GetExternalAccountKeys diff =\n%s", cmp.Diff(tc.resp, response, opts...))
}
})
}
}