package test import ( "RenewCMS/api" "RenewCMS/internal/domain/apikey" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "net/http" "testing" "time" "github.com/go-chi/chi/v5" ) type apiKeyCreatorLookup struct { user.Repository gets map[uint32]int failOn uint32 } func newAPIKeyCreatorLookup(inner user.Repository) *apiKeyCreatorLookup { return &apiKeyCreatorLookup{Repository: inner, gets: map[uint32]int{}} } func (a *apiKeyCreatorLookup) Get(id uint32) (user.User, error) { a.gets[id]++ if id == a.failOn { return user.User{}, errRepositoryFailure } return a.Repository.Get(id) } var _ user.Repository = &apiKeyCreatorLookup{} func (e *apiKeyEnv) useCreatorLookup(lookup *apiKeyCreatorLookup) { clock := func() time.Time { return e.now } e.container.ListAPIKeysUseCase = useCases.NewListAPIKeysUseCase(e.keys, lookup, clock) router := chi.NewRouter() router.Mount("/v1", api.InitBackendRoutes(e.container, e.settings)) e.router = router } func (e *apiKeyEnv) nameUsers() { owner := e.users.users[apiKeyOwnerId] owner.DisplayName = "Marta Alves" e.users.users[apiKeyOwnerId] = owner dev := e.users.users[apiKeyDevId] dev.DisplayName = "Dev Two" e.users.users[apiKeyDevId] = dev } func TestAPIKeysCreatorNameHttp(t *testing.T) { allFields := []string{"id", "user_id", "name", "prefix", "status", "created_at", "last_used_at", "expires_at", "revoked_at"} t.Run("API: an owner's list carries the creator's name", func(t *testing.T) { env := newAPIKeyEnv(t) env.nameUsers() env.issueKey(apiKeyOwnerId, "a") env.issueKey(apiKeyDevId, "b") env.issueKey(apiKeyPatId, "c") rec := env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId) assertAPIKeyStatus(t, rec, http.StatusOK) entries := apiKeyListedByName(t, rec) want := map[string]string{"a": "Marta Alves", "b": "Dev Two", "c": "pat"} for name, creator := range want { if entries[name]["created_by_name"] != creator { t.Errorf("entry %s: expected created_by_name %q, got %v", name, creator, entries[name]["created_by_name"]) } apiKeyAssertExactFields(t, entries[name], append([]string{"created_by_name"}, allFields...)...) } }) t.Run("API: a whitespace-only display name falls back to the username", func(t *testing.T) { env := newAPIKeyEnv(t) pat := env.users.users[apiKeyPatId] pat.DisplayName = " " env.users.users[apiKeyPatId] = pat env.issueKey(apiKeyPatId, "c") entries := apiKeyListedByName(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId)) if entries["c"]["created_by_name"] != "pat" { t.Errorf("expected pat, got %v", entries["c"]["created_by_name"]) } }) t.Run("API: an admin's list is unchanged", func(t *testing.T) { env := newAPIKeyEnv(t) env.nameUsers() env.issueKey(apiKeyDevId, "a") env.issueKey(apiKeyDevId, "b") rec := env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyDevId) assertAPIKeyStatus(t, rec, http.StatusOK) list := apiKeyDecodeList(t, rec) if len(list) != 2 { t.Fatalf("expected two entries, got %d", len(list)) } for _, entry := range list { apiKeyAssertExactFields(t, entry, allFields...) } }) t.Run("API: a key whose creator no longer exists", func(t *testing.T) { env := newAPIKeyEnv(t) env.issueKey(apiKeyPatId, "gone") env.issueKey(apiKeyDevId, "kept") delete(env.users.users, apiKeyPatId) rec := env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId) assertAPIKeyStatus(t, rec, http.StatusOK) entries := apiKeyListedByName(t, rec) if name, ok := entries["gone"]["created_by_name"]; !ok || name != "" { t.Errorf("expected an empty created_by_name, got %v", name) } if entries["kept"]["created_by_name"] != "dev" { t.Errorf("expected dev, got %v", entries["kept"]["created_by_name"]) } }) t.Run("API: a failing user lookup fails the list", func(t *testing.T) { env := newAPIKeyEnv(t) env.issueKey(apiKeyDevId, "a") lookup := newAPIKeyCreatorLookup(env.users) lookup.failOn = apiKeyDevId env.useCreatorLookup(lookup) rec := env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId) assertAPIKeyMessage(t, rec, http.StatusInternalServerError, "Internal server error") }) t.Run("API: the creator name is resolved once per user", func(t *testing.T) { env := newAPIKeyEnv(t) baseline := newAPIKeyCreatorLookup(env.users) env.useCreatorLookup(baseline) assertAPIKeyStatus(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId), http.StatusOK) env.issueKey(apiKeyOwnerId, "o1") env.issueKey(apiKeyOwnerId, "o2") env.issueKey(apiKeyOwnerId, "o3") env.issueKey(apiKeyDevId, "d1") env.issueKey(apiKeyDevId, "d2") lookup := newAPIKeyCreatorLookup(env.users) env.useCreatorLookup(lookup) assertAPIKeyStatus(t, env.withCookie(t, http.MethodGet, "/v1/api-key", "", apiKeyOwnerId), http.StatusOK) if lookup.gets[apiKeyDevId] != 1 { t.Errorf("expected one Get for the admin creator, got %d", lookup.gets[apiKeyDevId]) } if lookup.gets[apiKeyOwnerId] != baseline.gets[apiKeyOwnerId] { t.Errorf("expected no extra Get for the owner, got %d against %d", lookup.gets[apiKeyOwnerId], baseline.gets[apiKeyOwnerId]) } }) } func TestListAPIKeysCreatorName(t *testing.T) { seed := func() *mockAPIKeyRepo { repo := newMockAPIKeyRepo() seedStubKey(repo, "rcms_a", func(k *apikey.APIKey) { k.UserID = 1 }) seedStubKey(repo, "rcms_b", func(k *apikey.APIKey) { k.UserID = 2 }) return repo } users := func() *mockUserRepo { repo := apiKeyUnitUsers() marta := repo.users[1] marta.DisplayName = "Marta Alves" repo.users[1] = marta return repo } t.Run("Use case: ListAPIKeys returns the creator name only to an owner", func(t *testing.T) { uc := useCases.NewListAPIKeysUseCase(seed(), users(), apiKeyUnitClock) ownerEntries, err := uc.ListAPIKeys(1) if err != nil { t.Fatalf("unexpected error: %v", err) } names := map[string]string{} for _, entry := range ownerEntries { names[entry.Key.Prefix] = entry.CreatorName } if names["rcms_a"] != "Marta Alves" || names["rcms_b"] != "dev" { t.Errorf("unexpected creator names %v", names) } adminEntries, err := uc.ListAPIKeys(2) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(adminEntries) != 1 || adminEntries[0].CreatorName != "" { t.Errorf("expected one entry with an empty creator name, got %+v", adminEntries) } }) } type apiKeyDuplicateIdRepo struct { apikey.Repository keys []apikey.APIKey } func (a *apiKeyDuplicateIdRepo) ListAll() ([]apikey.APIKey, error) { return append([]apikey.APIKey(nil), a.keys...), nil } var _ apikey.Repository = &apiKeyDuplicateIdRepo{} func TestListAPIKeysTieKeepsRepositoryOrder(t *testing.T) { createdAt := apiKeyUnitNow.Add(-time.Hour) repo := &apiKeyDuplicateIdRepo{keys: []apikey.APIKey{ {ID: 7, UserID: 1, Name: "first", CreatedAt: createdAt}, {ID: 7, UserID: 1, Name: "second", CreatedAt: createdAt}, }} uc := useCases.NewListAPIKeysUseCase(repo, apiKeyUnitUsers(), apiKeyUnitClock) entries, err := uc.ListAPIKeys(1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(entries) != 2 || entries[0].Key.Name != "first" || entries[1].Key.Name != "second" { t.Errorf("expected a stable order for identical keys, got %+v", entries) } }