feat(SPA): screen to manage API keys (PART 2/3)

This commit is contained in:
fsylvain committed 2026-10-05 00:05:12 +02:00
1 parent 844842fdae
commit 514a082c96
30 files changed
+3844 -83

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
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)
}
}