diff --git a/acme/db/nosql/account.go b/acme/db/nosql/account.go index 1cf94b37..2f698d87 100644 --- a/acme/db/nosql/account.go +++ b/acme/db/nosql/account.go @@ -243,8 +243,7 @@ func (db *DB) DeleteExternalAccountKey(ctx context.Context, provisionerName stri return errors.Wrapf(err, "error loading ACME EAB Key with Key ID %s", keyID) } if dbeak.Provisioner != provisionerName { - // TODO: change these ACME error types; they don't make a lot of sense if used in the Admin APIs - return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + return errors.New("name of provisioner does not match provisioner for which the EAB key was created") } if dbeak.Reference != "" { err = db.db.Del(externalAccountKeysByReferenceTable, []byte(dbeak.Reference)) @@ -270,7 +269,7 @@ func (db *DB) GetExternalAccountKeys(ctx context.Context, provisionerName string for _, entry := range entries { dbeak := new(dbExternalAccountKey) if err = json.Unmarshal(entry.Value, dbeak); err != nil { - return nil, errors.Wrapf(err, "error unmarshaling external account key %s into dbExternalAccountKey", string(entry.Key)) + return nil, errors.Wrapf(err, "error unmarshaling external account key %s into ExternalAccountKey", string(entry.Key)) } if dbeak.Provisioner != provisionerName { continue @@ -314,7 +313,7 @@ func (db *DB) UpdateExternalAccountKey(ctx context.Context, provisionerName stri } if old.Provisioner != provisionerName { - return acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created") + return errors.New("name of provisioner does not match provisioner for which the EAB key was created") } nu := dbExternalAccountKey{ diff --git a/acme/db/nosql/account_test.go b/acme/db/nosql/account_test.go index 5ba99a73..08b60f58 100644 --- a/acme/db/nosql/account_test.go +++ b/acme/db/nosql/account_test.go @@ -704,3 +704,1020 @@ func TestDB_UpdateAccount(t *testing.T) { }) } } + +func TestDB_getDBExternalAccountKey(t *testing.T) { + keyID := "keyID" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + dbeak *dbExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "prov", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: nil, + dbeak: dbeak, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: acme.ErrNotFound, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return []byte("foo"), nil + }, + }, + err: errors.New("error unmarshaling external account key keyID into dbExternalAccountKey"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if dbeak, err := db.getDBExternalAccountKey(context.Background(), keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.dbeak.ID) + assert.Equals(t, dbeak.KeyBytes, tc.dbeak.KeyBytes) + assert.Equals(t, dbeak.Provisioner, tc.dbeak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.dbeak.Reference) + assert.Equals(t, dbeak.CreatedAt, tc.dbeak.CreatedAt) + assert.Equals(t, dbeak.AccountID, tc.dbeak.AccountID) + assert.Equals(t, dbeak.BoundAt, tc.dbeak.BoundAt) + } + } + }) + } +} + +func TestDB_GetExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "aDifferentProv", + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + acmeErr: acme.NewError(acme.ErrorUnauthorizedType, "name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eak, err := db.GetExternalAccountKey(context.Background(), prov, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + } + } + }) + } +} + +func TestDB_GetExternalAccountKeyByReference(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + ref string + acmeErr *acme.Error + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + eak: &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + err: nil, + } + }, + "ok/no-reference": func(t *testing.T) test { + return test{ + ref: "", + eak: nil, + err: nil, + } + }, + "fail/reference-not-found": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("ACME EAB key for reference ref not found"), + } + }, + "fail/reference-load-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return nil, errors.New("force") + }, + }, + err: errors.New("error loading ACME EAB key for reference ref: force"), + } + }, + "fail/reference-unmarshal-error": func(t *testing.T) test { + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeysByReferenceTable)) + assert.Equals(t, string(key), ref) + return []byte{0}, nil + }, + }, + err: errors.New("error unmarshaling ACME EAB key for reference ref"), + } + }, + "fail/db.GetExternalAccountKey-error": func(t *testing.T) test { + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + ref: ref, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eak, err := db.GetExternalAccountKeyByReference(context.Background(), prov, tc.ref); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) && tc.eak != nil { + assert.Equals(t, eak.ID, tc.eak.ID) + assert.Equals(t, eak.AccountID, tc.eak.AccountID) + assert.Equals(t, eak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, eak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, eak.KeyBytes, tc.eak.KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, eak.Reference, tc.eak.Reference) + } + } + }) + } +} + +func TestDB_GetExternalAccountKeys(t *testing.T) { + keyID1 := "keyID1" + keyID2 := "keyID2" + keyID3 := "keyID3" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + eaks []*acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak1 := &dbExternalAccountKey{ + ID: keyID1, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b1, err := json.Marshal(dbeak1) + assert.FatalError(t, err) + dbeak2 := &dbExternalAccountKey{ + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b2, err := json.Marshal(dbeak2) + assert.FatalError(t, err) + dbeak3 := &dbExternalAccountKey{ + ID: keyID3, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b3, err := json.Marshal(dbeak3) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: b1, + }, + { + Bucket: bucket, + Key: []byte(keyID2), + Value: b2, + }, + { + Bucket: bucket, + Key: []byte(keyID3), + Value: b3, + }, + }, nil + }, + }, + eaks: []*acme.ExternalAccountKey{ + { + ID: keyID1, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + { + ID: keyID2, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + }, + }, + } + }, + "fail/db.List-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + return nil, errors.New("force") + }, + }, + err: errors.New("force"), + } + }, + "fail/unmarshal-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MList: func(bucket []byte) ([]*nosqldb.Entry, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + return []*nosqldb.Entry{ + { + Bucket: bucket, + Key: []byte(keyID1), + Value: []byte("foo"), + }, + }, nil + }, + }, + eaks: []*acme.ExternalAccountKey{}, + err: errors.Errorf("error unmarshaling external account key %s into ExternalAccountKey", keyID1), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if eaks, err := db.GetExternalAccountKeys(context.Background(), prov); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, len(eaks), len(tc.eaks)) + for i, eak := range eaks { + assert.Equals(t, eak.ID, tc.eaks[i].ID) + assert.Equals(t, eak.KeyBytes, tc.eaks[i].KeyBytes) + assert.Equals(t, eak.Provisioner, tc.eaks[i].Provisioner) + assert.Equals(t, eak.Reference, tc.eaks[i].Reference) + assert.Equals(t, eak.CreatedAt, tc.eaks[i].CreatedAt) + assert.Equals(t, eak.AccountID, tc.eaks[i].AccountID) + assert.Equals(t, eak.BoundAt, tc.eaks[i].BoundAt) + } + } + } + }) + } +} + +func TestDB_DeleteExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + acmeErr *acme.Error + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + } + }, + "fail/not-found": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return nil, nosqldb.ErrNotFound + }, + }, + err: errors.New("error loading ACME EAB Key with Key ID keyID"), + } + }, + "fail/non-matching-provisioner": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, string(bucket), string(externalAccountKeyTable)) + assert.Equals(t, string(key), keyID) + return b, nil + }, + }, + err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/delete-reference": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + err: errors.New("error deleting ACME EAB Key Reference with Key ID keyID and reference ref"), + } + }, + "fail/delete-eak": func(t *testing.T) test { + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + dbref := &dbExternalAccountKeyReference{ + Reference: ref, + ExternalAccountKeyID: keyID, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + dbrefBytes, err := json.Marshal(dbref) + assert.FatalError(t, err) + return test{ + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return dbrefBytes, nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return b, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, errors.New("force") + } + }, + MDel: func(bucket, key []byte) error { + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + return nil + case string(externalAccountKeyTable): + assert.Equals(t, string(key), keyID) + return errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return errors.New("force") + } + }, + }, + err: errors.New("error deleting ACME EAB Key with Key ID keyID"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if err := db.DeleteExternalAccountKey(context.Background(), prov, keyID); err != nil { + switch k := err.(type) { + case *acme.Error: + if assert.NotNil(t, tc.acmeErr) { + assert.Equals(t, k.Type, tc.acmeErr.Type) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + assert.Equals(t, k.Status, tc.acmeErr.Status) + assert.Equals(t, k.Err.Error(), tc.acmeErr.Err.Error()) + assert.Equals(t, k.Detail, tc.acmeErr.Detail) + } + default: + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } + } else { + assert.Nil(t, tc.err) + } + }) + } +} + +func TestDB_CreateExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + type test struct { + db nosql.DB + err error + _id *string + eak *acme.ExternalAccountKey + } + var tests = map[string]func(t *testing.T) test{ + "ok": func(t *testing.T) test { + var ( + id string + idPtr = &id + ) + now := clock.Now() + eak := &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: "ref", + AccountID: "", + CreatedAt: now, + } + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + + id = string(key) + + dbeak := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbeak)) + assert.Equals(t, string(key), dbeak.ID) + assert.Equals(t, eak.Provisioner, dbeak.Provisioner) + assert.Equals(t, eak.Reference, dbeak.Reference) + assert.Equals(t, 32, len(dbeak.KeyBytes)) + assert.False(t, dbeak.CreatedAt.IsZero()) + assert.Equals(t, dbeak.AccountID, eak.AccountID) + assert.True(t, dbeak.BoundAt.IsZero()) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + eak: eak, + _id: idPtr, + } + }, + "fail/externalAccountKeyID-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, nil + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + err: errors.New("error saving acme external_account_key"), + } + }, + "fail/externalAccountKeyReference-cmpAndSwap-error": func(t *testing.T) test { + return test{ + db: &db.MockNoSQLDB{ + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + + switch string(bucket) { + case string(externalAccountKeysByReferenceTable): + assert.Equals(t, string(key), ref) + assert.Equals(t, old, nil) + return nu, true, errors.New("force") + case string(externalAccountKeyTable): + assert.Equals(t, old, nil) + return nu, true, nil + default: + assert.FatalError(t, errors.Errorf("unrecognized bucket %s", string(bucket))) + return nil, false, errors.New("force") + } + }, + }, + err: errors.New("error saving acme external_account_key"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + eak, err := db.CreateExternalAccountKey(context.Background(), prov, ref) + if err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, *tc._id, eak.ID) + assert.Equals(t, prov, eak.Provisioner) + assert.Equals(t, ref, eak.Reference) + assert.Equals(t, "", eak.AccountID) + assert.False(t, eak.CreatedAt.IsZero()) + assert.False(t, eak.AlreadyBound()) + assert.True(t, eak.BoundAt.IsZero()) + } + } + }) + } +} + +func TestDB_UpdateExternalAccountKey(t *testing.T) { + keyID := "keyID" + prov := "acmeProv" + ref := "ref" + now := clock.Now() + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + type test struct { + db nosql.DB + eak *acme.ExternalAccountKey + err error + } + var tests = map[string]func(t *testing.T) test{ + + "ok": func(t *testing.T) test { + eak := &acme.ExternalAccountKey{ + ID: keyID, + Provisioner: prov, + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + return test{ + eak: eak, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + MCmpAndSwap: func(bucket, key, old, nu []byte) ([]byte, bool, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, old, b) + + dbNew := new(dbExternalAccountKey) + assert.FatalError(t, json.Unmarshal(nu, dbNew)) + assert.Equals(t, dbNew.ID, dbeak.ID) + assert.Equals(t, dbNew.Provisioner, dbeak.Provisioner) + assert.Equals(t, dbNew.Reference, dbeak.Reference) + assert.Equals(t, dbNew.AccountID, dbeak.AccountID) + assert.Equals(t, dbNew.CreatedAt, dbeak.CreatedAt) + assert.Equals(t, dbNew.BoundAt, dbeak.BoundAt) + assert.Equals(t, dbNew.KeyBytes, dbeak.KeyBytes) + return nu, true, nil + }, + }, + } + }, + "fail/provisioner-mismatch": func(t *testing.T) test { + dbeak := &dbExternalAccountKey{ + ID: keyID, + Provisioner: "differentProvisioner", + Reference: ref, + AccountID: "", + KeyBytes: []byte{1, 3, 3, 7}, + CreatedAt: now, + } + b, err := json.Marshal(dbeak) + assert.FatalError(t, err) + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return b, nil + }, + }, + err: errors.New("name of provisioner does not match provisioner for which the EAB key was created"), + } + }, + "fail/db.Get-error": func(t *testing.T) test { + return test{ + eak: &acme.ExternalAccountKey{ + ID: keyID, + }, + db: &db.MockNoSQLDB{ + MGet: func(bucket, key []byte) ([]byte, error) { + assert.Equals(t, bucket, externalAccountKeyTable) + assert.Equals(t, string(key), keyID) + + return nil, errors.New("force") + }, + }, + err: errors.New("error loading external account key keyID: force"), + } + }, + } + for name, run := range tests { + tc := run(t) + t.Run(name, func(t *testing.T) { + db := DB{db: tc.db} + if err := db.UpdateExternalAccountKey(context.Background(), prov, tc.eak); err != nil { + if assert.NotNil(t, tc.err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + if assert.Nil(t, tc.err) { + assert.Equals(t, dbeak.ID, tc.eak.ID) + assert.Equals(t, dbeak.Provisioner, tc.eak.Provisioner) + assert.Equals(t, dbeak.Reference, tc.eak.Reference) + assert.Equals(t, dbeak.AccountID, tc.eak.AccountID) + assert.Equals(t, dbeak.CreatedAt, tc.eak.CreatedAt) + assert.Equals(t, dbeak.BoundAt, tc.eak.BoundAt) + assert.Equals(t, dbeak.KeyBytes, tc.eak.KeyBytes) + } + } + }) + } +} diff --git a/authority/admin/api/acme.go b/authority/admin/api/acme.go index 700881dc..a54243a4 100644 --- a/authority/admin/api/acme.go +++ b/authority/admin/api/acme.go @@ -137,13 +137,7 @@ func (h *Handler) GetExternalAccountKeys(w http.ResponseWriter, r *http.Request) prov := chi.URLParam(r, "prov") reference := chi.URLParam(r, "ref") - // TODO: support paging properly? It'll probably leak to the DB layer, as we have to loop through all keys - // cursor, limit, err := api.ParseCursor(r) - // if err != nil { - // api.WriteError(w, admin.WrapError(admin.ErrorBadRequestType, err, - // "error parsing cursor and limit from query params")) - // return - // } + // TODO: support paging? It'll probably leak to the DB layer, as we have to loop through all keys var ( key *acme.ExternalAccountKey