@ -10,19 +10,14 @@ import (
"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 {
@ -37,7 +32,7 @@ func readProtoJSON(r io.ReadCloser, m proto.Message) error {
func TestHandler_requireEABEnabled ( t * testing . T ) {
type test struct {
ctx context . Context
db admin . DB
adminDB admin . DB
auth adminAuthority
next nextHTTP
err * admin . Error
@ -98,7 +93,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
return test {
ctx : ctx ,
auth : auth ,
db: db ,
adminDB: db ,
err : err ,
statusCode : 400 ,
}
@ -134,9 +129,9 @@ func TestHandler_requireEABEnabled(t *testing.T) {
} ,
}
return test {
ctx : ctx ,
auth : auth ,
db: db ,
ctx : ctx ,
auth : auth ,
adminDB: db ,
next : func ( w http . ResponseWriter , r * http . Request ) {
w . Write ( nil ) // mock response with status 200
} ,
@ -149,9 +144,9 @@ func TestHandler_requireEABEnabled(t *testing.T) {
tc := prep ( t )
t . Run ( name , func ( t * testing . T ) {
h := & Handler {
db: tc . db ,
a uth: tc . auth ,
acmeDB : nil ,
auth: tc . auth ,
a dminDB: tc . adminDB ,
acmeDB : nil ,
}
req := httptest . NewRequest ( "GET" , "/foo" , nil ) // chi routing is prepared in test setup
@ -183,7 +178,7 @@ func TestHandler_requireEABEnabled(t *testing.T) {
func TestHandler_provisionerHasEABEnabled ( t * testing . T ) {
type test struct {
db admin . DB
adminDB admin . DB
auth adminAuthority
provisionerName string
want bool
@ -223,7 +218,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
}
return test {
auth : auth ,
db: db ,
adminDB: db ,
provisionerName : "provName" ,
want : false ,
err : admin . WrapErrorISE ( errors . New ( "force" ) , "error loading provisioner provName" ) ,
@ -252,7 +247,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
}
return test {
auth : auth ,
db: db ,
adminDB: db ,
provisionerName : "provName" ,
want : false ,
err : admin . WrapErrorISE ( errors . New ( "force" ) , "error loading provisioner provName" ) ,
@ -285,7 +280,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
}
return test {
auth : auth ,
db: db ,
adminDB: db ,
provisionerName : "provName" ,
want : false ,
err : admin . WrapErrorISE ( errors . New ( "force" ) , "error loading provisioner provName" ) ,
@ -319,7 +314,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
} ,
}
return test {
db: db ,
adminDB: db ,
auth : auth ,
provisionerName : "eab-disabled" ,
want : false ,
@ -353,7 +348,7 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
} ,
}
return test {
db: db ,
adminDB: db ,
auth : auth ,
provisionerName : "eab-enabled" ,
want : true ,
@ -364,9 +359,9 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
tc := prep ( t )
t . Run ( name , func ( t * testing . T ) {
h := & Handler {
db: tc . db ,
a uth: tc . auth ,
acmeDB : nil ,
auth: tc . auth ,
a dminDB: tc . adminDB ,
acmeDB : nil ,
}
got , prov , err := h . provisionerHasEABEnabled ( context . TODO ( ) , tc . provisionerName )
if ( err != nil ) != ( tc . err != nil ) {
@ -391,54 +386,6 @@ func TestHandler_provisionerHasEABEnabled(t *testing.T) {
}
}
func Test_provisionerFromContext ( t * testing . T ) {
prov := & linkedca . Provisioner {
Id : "provID" ,
Name : "acmeProv" ,
}
tests := [ ] struct {
name string
ctx context . Context
want * linkedca . Provisioner
wantErr bool
} {
{
name : "fail/no-provisioner" ,
ctx : context . Background ( ) ,
want : nil ,
wantErr : true ,
} ,
{
name : "fail/wrong-type" ,
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , "prov" ) ,
want : nil ,
wantErr : true ,
} ,
{
name : "ok" ,
ctx : context . WithValue ( context . Background ( ) , provisionerContextKey , prov ) ,
want : & linkedca . Provisioner {
Id : "provID" ,
Name : "acmeProv" ,
} ,
wantErr : false ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
got , err := provisionerFromContext ( tt . ctx )
if ( err != nil ) != tt . wantErr {
t . Errorf ( "provisionerFromContext() error = %v, wantErr %v" , err , tt . wantErr )
return
}
opts := [ ] cmp . Option { cmpopts . IgnoreUnexported ( linkedca . Provisioner { } ) }
if ! cmp . Equal ( tt . want , got , opts ... ) {
t . Errorf ( "provisionerFromContext() diff =\n %s" , cmp . Diff ( tt . want , got , opts ... ) )
}
} )
}
}
func TestCreateExternalAccountKeyRequest_Validate ( t * testing . T ) {
type fields struct {
Reference string
@ -483,740 +430,158 @@ func TestCreateExternalAccountKeyRequest_Validate(t *testing.T) {
}
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 ( "provisionerName" , "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 ( "provisionerName" , "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/no-provisioner-in-context" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "provName" )
ctx := context . WithValue ( context . Background ( ) , chi . RouteCtxKey , chiCtx )
req := CreateExternalAccountKeyRequest {
Reference : "aRef" ,
}
body , err := json . Marshal ( req )
assert . FatalError ( t , err )
return test {
ctx : ctx ,
body : body ,
statusCode : 500 ,
eak : nil ,
err : & admin . Error {
Type : admin . ErrorServerInternalType . String ( ) ,
Status : 500 ,
Detail : "the server experienced an internal error" ,
Message : "error getting provisioner from context: provisioner expected in request context" ,
} ,
}
} ,
"fail/acmeDB.GetExternalAccountKeyByReference" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "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 ( "provisionerName" , "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 ( "provisionerName" , "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 {
"ok" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "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 ,
statusCode : 501 ,
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 ( "provisionerName" , "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 ( "provisionerName" , "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 } ,
Type : admin . ErrorNotImplementedType . String ( ) ,
Status : http . StatusNotImplemented ,
Message : "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm" ,
Detail : "not implemented" ,
} ,
}
} ,
}
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 := httptest . NewRequest ( "POST" , "/foo" , nil ) // chi routing is prepared in test setup
req = req . WithContext ( tc . ctx )
w := httptest . NewRecorder ( )
h . CreateExternalAccountKey ( w , req )
acmeResponder := NewACMEAdminResponder ( )
acmeResponder . 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 ) )
body , err := io . ReadAll ( res . Body )
res . Body . Close ( )
assert . FatalError ( t , err )
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
}
adminErr := admin . Error { }
assert . FatalError ( t , json . Unmarshal ( bytes . TrimSpace ( body ) , & adminErr ) )
eabKey := & linkedca . EABKey { }
err := readProtoJSON ( res . Body , eabKey )
assert . FatalError ( t , err )
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" ] )
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/no-provisioner-in-context" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "provName" )
ctx := context . WithValue ( context . Background ( ) , chi . RouteCtxKey , chiCtx )
return test {
ctx : ctx ,
statusCode : 500 ,
err : & admin . Error {
Type : admin . ErrorServerInternalType . String ( ) ,
Status : 500 ,
Detail : "the server experienced an internal error" ,
Message : "error getting provisioner from context: provisioner expected in request context" ,
} ,
}
} ,
"fail/acmeDB.DeleteExternalAccountKey" : func ( t * testing . T ) test {
"ok" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "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 ,
statusCode : 501 ,
err : & admin . Error {
Type : admin . Error ServerInternal Type. String ( ) ,
Status : 500 ,
Detail: "the server experienced an internal error ",
Message: "error deleting ACME EAB Key 'keyID': force ",
Type : admin . ErrorNotImplementedType . String ( ) ,
Status : http . StatusNotImplemented ,
Message : "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm" ,
Detail : "not implemented" ,
} ,
}
} ,
"ok" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "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 )
acmeResponder := NewACMEAdminResponder ( )
acmeResponder . 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" ] )
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" ] )
} )
}
}
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/no-provisioner-in-context" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "provName" )
ctx := context . WithValue ( context . Background ( ) , chi . RouteCtxKey , chiCtx )
req := httptest . NewRequest ( "GET" , "/foo" , nil )
return test {
ctx : ctx ,
statusCode : 500 ,
req : req ,
err : & admin . Error {
Type : admin . ErrorServerInternalType . String ( ) ,
Status : 500 ,
Detail : "the server experienced an internal error" ,
Message : "error getting provisioner from context: provisioner expected in request context" ,
} ,
}
} ,
"fail/parse-cursor" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "provName" )
req := httptest . NewRequest ( "GET" , "/foo?limit=A" , nil )
ctx := context . WithValue ( context . Background ( ) , chi . RouteCtxKey , chiCtx )
ctx = context . WithValue ( ctx , provisionerContextKey , prov )
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 ( "provisionerName" , "provName" )
chiCtx . URLParams . Add ( "reference" , "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 {
"ok" : func ( t * testing . T ) test {
chiCtx := chi . NewRouteContext ( )
chiCtx . URLParams . Add ( "provisionerName" , "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 , cursor string , limit int ) ( [ ] * acme . ExternalAccountKey , string , error ) {
assert . Equals ( t , "provID" , provisionerID )
assert . Equals ( t , "" , cursor )
assert . Equals ( t , 0 , limit )
return nil , "" , errors . New ( "force" )
} ,
}
return test {
ctx : ctx ,
statusCode : 50 0 ,
statusCode : 501 ,
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 ( "provisionerName" , "provName" )
chiCtx . URLParams . Add ( "reference" , "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 ( "provisionerName" , "provName" )
chiCtx . URLParams . Add ( "reference" , "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 ( "provisionerName" , "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 , cursor string , limit int ) ( [ ] * acme . ExternalAccountKey , string , error ) {
assert . Equals ( t , "provID" , provisionerID )
assert . Equals ( t , "" , cursor )
assert . Equals ( t , 0 , limit )
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" ,
} ,
} ,
Type : admin . ErrorNotImplementedType . String ( ) ,
Status : http . StatusNotImplemented ,
Message : "this functionality is currently only available in Certificate Manager: https://u.step.sm/cm" ,
Detail : "not implemented" ,
} ,
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 )
acmeResponder := NewACMEAdminResponder ( )
acmeResponder . 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 ) )
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" ] )
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 ... ) )
}
} )
}
}