diff --git a/authority/admin/api/acme_test.go b/authority/admin/api/acme_test.go index 2c09b06f..114629fc 100644 --- a/authority/admin/api/acme_test.go +++ b/authority/admin/api/acme_test.go @@ -723,18 +723,19 @@ func TestHandler_CreateExternalAccountKey(t *testing.T) { 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...)) - } + 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...)) + } + }) } } @@ -817,16 +818,18 @@ func TestHandler_DeleteExternalAccountKey(t *testing.T) { 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"]) + 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"]) + }) } } diff --git a/authority/admin/api/admin_test.go b/authority/admin/api/admin_test.go new file mode 100644 index 00000000..da044d58 --- /dev/null +++ b/authority/admin/api/admin_test.go @@ -0,0 +1,811 @@ +package api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/http/httptest" + "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/api" + "github.com/smallstep/certificates/authority/admin" + "github.com/smallstep/certificates/authority/provisioner" + "go.step.sm/linkedca" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestCreateAdminRequest_Validate(t *testing.T) { + type fields struct { + Subject string + Provisioner string + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/subject-empty", + fields: fields{ + Subject: "", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "subject cannot be empty"), + }, + { + name: "fail/provisioner-empty", + fields: fields{ + Subject: "admin", + Provisioner: "", + Type: 0, + }, + err: admin.NewError(admin.ErrorBadRequestType, "provisioner cannot be empty"), + }, + { + name: "fail/invalid-type", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + car := &CreateAdminRequest{ + Subject: tt.fields.Subject, + Provisioner: tt.fields.Provisioner, + Type: tt.fields.Type, + } + err := car.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestUpdateAdminRequest_Validate(t *testing.T) { + type fields struct { + Type linkedca.Admin_Type + } + tests := []struct { + name string + fields fields + err *admin.Error + }{ + { + name: "fail/invalid-type", + fields: fields{ + Type: -1, + }, + err: admin.NewError(admin.ErrorBadRequestType, "invalid value for admin type"), + }, + { + name: "ok", + fields: fields{ + Type: linkedca.Admin_SUPER_ADMIN, + }, + err: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uar := &UpdateAdminRequest{ + Type: tt.fields.Type, + } + + err := uar.Validate() + + if (err != nil) != (tt.err != nil) { + t.Errorf("CreateAdminRequest.Validate() error = %v, wantErr %v", err, (tt.err != nil)) + return + } + + if err != nil { + assert.Type(t, &admin.Error{}, err) + adminErr, _ := err.(*admin.Error) + assert.Equals(t, tt.err.Type, adminErr.Type) + assert.Equals(t, tt.err.Detail, adminErr.Detail) + assert.Equals(t, tt.err.Status, adminErr.Status) + assert.Equals(t, tt.err.Message, adminErr.Message) + } + }) + } +} + +func TestHandler_GetAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.LoadAdminByID-not-found": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return nil, false + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 404, + err: &admin.Error{ + Type: admin.ErrorNotFoundType.String(), + Status: 404, + Detail: "resource not found", + Message: "admin adminID not found", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + createdAt := time.Now() + var deletedAt time.Time + adm := &linkedca.Admin{ + Id: "adminID", + AuthorityId: "authorityID", + Subject: "admin", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &api.MockAuthority{ + MockLoadAdminByID: func(id string) (*linkedca.Admin, bool) { + assert.Equals(t, "adminID", id) + return adm, true + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := httptest.NewRequest("GET", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmin(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.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("linkedca.Admin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_GetAdmins(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + req *http.Request + statusCode int + err *admin.Error + resp GetAdminsResponse + } + var tests = map[string]func(t *testing.T) test{ + "fail/parse-cursor": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo?limit=A", nil) + return test{ + ctx: context.Background(), + req: req, + statusCode: 400, + 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/auth.GetAdmins": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + auth := &api.MockAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return nil, "", errors.New("force") + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Status: 500, + Type: admin.ErrorServerInternalType.String(), + Detail: "the server experienced an internal error", + Message: "error retrieving paginated admins: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := httptest.NewRequest("GET", "/foo", nil) + createdAt := time.Now() + var deletedAt time.Time + adm1 := &linkedca.Admin{ + Id: "adminID1", + AuthorityId: "authorityID1", + Subject: "admin1", + ProvisionerId: "provID", + Type: linkedca.Admin_SUPER_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + adm2 := &linkedca.Admin{ + Id: "adminID2", + AuthorityId: "authorityID", + Subject: "admin2", + ProvisionerId: "provID", + Type: linkedca.Admin_ADMIN, + CreatedAt: timestamppb.New(createdAt), + DeletedAt: timestamppb.New(deletedAt), + } + auth := &api.MockAuthority{ + MockGetAdmins: func(cursor string, limit int) ([]*linkedca.Admin, string, error) { + assert.Equals(t, "", cursor) + assert.Equals(t, 0, limit) + return []*linkedca.Admin{ + adm1, + adm2, + }, "nextCursorValue", nil + }, + } + return test{ + ctx: context.Background(), + req: req, + auth: auth, + statusCode: 200, + err: nil, + resp: GetAdminsResponse{ + Admins: []*linkedca.Admin{ + adm1, + adm2, + }, + NextCursor: "nextCursorValue", + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + + req := tc.req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.GetAdmins(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 { + + 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.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + response := GetAdminsResponse{} + 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.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.resp, response, opts...) { + t.Errorf("GetAdmins diff =\n%s", cmp.Diff(tc.resp, response, opts...)) + } + }) + } +} + +func TestHandler_CreateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + 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 { + req := CreateAdminRequest{ + Subject: "", + Provisioner: "", + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "subject cannot be empty", + }, + } + }, + "fail/auth.LoadProvisionerByName": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return nil, errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error loading provisioner prov: force", + }, + } + }, + "fail/auth.StoreAdmin": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return errors.New("force") + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error storing admin: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := CreateAdminRequest{ + Subject: "admin", + Provisioner: "prov", + Type: linkedca.Admin_SUPER_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + auth := &api.MockAuthority{ + MockLoadProvisionerByName: func(name string) (provisioner.Interface, error) { + assert.Equals(t, "prov", name) + return &provisioner.ACME{ + ID: "provID", + Name: "prov", + }, nil + }, + MockStoreAdmin: func(ctx context.Context, adm *linkedca.Admin, prov provisioner.Interface) error { + assert.Equals(t, "admin", adm.Subject) + assert.Equals(t, "provID", prov.GetID()) + return nil + }, + } + return test{ + ctx: context.Background(), + body: body, + auth: auth, + statusCode: 201, + err: nil, + adm: &linkedca.Admin{ + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + }, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.CreateAdmin(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.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.CreateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +} + +func TestHandler_DeleteAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + statusCode int + err *admin.Error + } + var tests = map[string]func(t *testing.T) test{ + "fail/auth.RemoveAdmin": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return errors.New("force") + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error deleting admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockRemoveAdmin: func(ctx context.Context, id string) error { + assert.Equals(t, "adminID", id) + return nil + }, + } + return test{ + ctx: ctx, + auth: auth, + statusCode: 200, + err: nil, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("DELETE", "/foo", nil) // chi routing is prepared in test setup + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.DeleteAdmin(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_UpdateAdmin(t *testing.T) { + type test struct { + ctx context.Context + auth api.LinkedAuthority + body []byte + statusCode int + err *admin.Error + adm *linkedca.Admin + } + var tests = map[string]func(t *testing.T) test{ + "fail/ReadJSON": func(t *testing.T) test { + body := []byte("{!?}") + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + 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 { + req := UpdateAdminRequest{ + Type: -1, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + return test{ + ctx: context.Background(), + body: body, + statusCode: 400, + err: &admin.Error{ + Type: admin.ErrorBadRequestType.String(), + Status: 400, + Detail: "bad request", + Message: "invalid value for admin type", + }, + } + }, + "fail/auth.UpdateAdmin": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + auth := &api.MockAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return nil, errors.New("force") + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 500, + err: &admin.Error{ + Type: admin.ErrorServerInternalType.String(), + Status: 500, + Detail: "the server experienced an internal error", + Message: "error updating admin adminID: force", + }, + } + }, + "ok": func(t *testing.T) test { + req := UpdateAdminRequest{ + Type: linkedca.Admin_ADMIN, + } + body, err := json.Marshal(req) + assert.FatalError(t, err) + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("id", "adminID") + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + adm := &linkedca.Admin{ + Id: "adminID", + ProvisionerId: "provID", + Subject: "admin", + Type: linkedca.Admin_SUPER_ADMIN, + } + auth := &api.MockAuthority{ + MockUpdateAdmin: func(ctx context.Context, id string, nu *linkedca.Admin) (*linkedca.Admin, error) { + assert.Equals(t, "adminID", id) + assert.Equals(t, linkedca.Admin_ADMIN, nu.Type) + return adm, nil + }, + } + return test{ + ctx: ctx, + body: body, + auth: auth, + statusCode: 200, + err: nil, + adm: adm, + } + }, + } + for name, prep := range tests { + tc := prep(t) + t.Run(name, func(t *testing.T) { + h := &Handler{ + auth: tc.auth, + } + req := httptest.NewRequest("GET", "/foo", io.NopCloser(bytes.NewBuffer(tc.body))) + req = req.WithContext(tc.ctx) + w := httptest.NewRecorder() + h.UpdateAdmin(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.Detail, adminErr.Detail) + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + return + } + + adm := &linkedca.Admin{} + err := readProtoJSON(res.Body, adm) + assert.FatalError(t, err) + + assert.Equals(t, []string{"application/json"}, res.Header["Content-Type"]) + + opts := []cmp.Option{cmpopts.IgnoreUnexported(linkedca.Admin{}, timestamppb.Timestamp{})} + if !cmp.Equal(tc.adm, adm, opts...) { + t.Errorf("h.UpdateAdmin diff =\n%s", cmp.Diff(tc.adm, adm, opts...)) + } + }) + } +}