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...))
+			}
+		})
+	}
+}