td-sync Admin API Integration Tests
Write integration tests in internal/api/admin_integration_test.go using the harness in internal/api/testharness_test.go. Tests run against a real HTTP server on a random port with isolated temp databases.
Quick Start Pattern
func TestIntegration_DescriptiveName(t *testing.T) {
t.Parallel()
h := newTestHarness(t)
state := h.Build().
WithUser("user@test.com").
WithAdmin("admin@test.com", "admin:read:server,sync").
WithProject("proj1", "user@test.com").
WithEvents("proj1", "user@test.com", 5).
Done()
token := state.AdminToken("admin@test.com")
pid := state.ProjectID("proj1")
var resp adminEventsResponse
h.DoJSON("GET", fmt.Sprintf("/v1/admin/projects/%s/events", pid), token, nil, &resp)
if len(resp.Data) != 5 {
t.Fatalf("expected 5 events, got %d", len(resp.Data))
}
}
Harness API
See references/harness-api.md for the complete API reference with all method signatures and detailed usage notes.
Core
newTestHarness(t, ...func(*Config)) *TestHarness -- real HTTP server, isolated DB, auto-cleanup
h.Do(method, path, token, body) *http.Response -- real HTTP request (caller closes body)
h.DoJSON(method, path, token, body, &out) *http.Response -- request + JSON decode (fatals on 4xx/5xx)
State Builder
h.Build().
WithUser(email). // sync-scoped key
WithAdmin(email, scopes). // admin key with scopes
WithProject(name, ownerEmail). // via API (owner must exist)
WithMember(projectName, email, role). // "owner"/"writer"/"reader"
WithEvents(projectName, userEmail, count).// cycles issues/logs/comments
WithSnapshot(projectName). // triggers snapshot build
WithAuthEvents(count). // inserts directly to DB
WithRateLimitEvents(count). // inserts directly to DB
Done() // -> *TestState
Ordering matters: create users before projects, projects before members/events/snapshots.
State Accessors
state.UserToken(email), state.UserID(email), state.AdminToken(email), state.ProjectID(name), state.Harness()
Assertions
AssertStatus(t, resp, 200) -- checks status, prints body on failure
AssertErrorResponse(t, resp, 403, "insufficient_admin_scope") -- checks status + error code
ReadJSON[T](t, resp) T -- generic JSON decode
AssertPaginated[T](t, resp, count, hasMore) PaginatedResponse[T] -- checks paginated list
AssertCORSHeaders(t, resp, origin) / AssertNoCORSHeaders(t, resp)
h.AssertRequiresAdminScope(t, method, path, wrongToken) -- 403 + error code check
Admin Scopes
| Scope |
Endpoints |
admin:read:server |
server/overview, server/config, rate-limit-violations, users, users/{id}, users/{id}/keys, auth/events |
admin:read:projects |
projects, projects/{id}, projects/{id}/members, sync/status, sync/cursors |
admin:read:events |
projects/{id}/events, projects/{id}/events/{seq}, entity-types |
admin:read:snapshots |
projects/{id}/snapshot/meta, projects/{id}/snapshot/query |
admin:export |
projects/{id}/events/export |
Response Types
Internal types accessible from test files in package api:
serverOverviewResponse -- server overview
serverConfigResponse -- server config
adminEventsResponse -- {Data []adminEvent, HasMore bool}
adminEvent -- single event: ServerSeq, EntityType, EntityID, ActionType, Payload
adminSyncStatusResponse -- {EventCount, LastServerSeq, LastEventTime}
adminCursorEntry -- {ClientID, LastEventID, LastSyncAt, DistanceFromHead}
serverdb.AdminProject -- project: ID, Name, MemberCount, EventCount
serverdb.AdminUser -- user: ID, Email, IsAdmin, ProjectCount
serverdb.AdminProjectMember -- {UserID, Email, Role}
Rules
- Always
t.Parallel() -- each harness is isolated
- Test name prefix:
TestIntegration_
- Config overrides via opts:
newTestHarness(t, func(cfg *Config) { cfg.CORSAllowedOrigins = []string{"https://x.com"} })
- First user created is auto-admin; consume with
h.CreateUser("first@test.com") when testing non-admin denial
- CORS tests need manual
http.NewRequest since Do doesn't support custom headers
- Run tests:
go test -v -run TestIntegration ./internal/api/
1---2name: td-integration-test3description: Write integration tests for the td-sync admin API using the TestHarness in internal/api/testharness_test.go. Use when asked to write, add, or fix integration tests for admin API endpoints (server, users, projects, events, snapshots, CORS, auth). The harness provides a real HTTP server, fluent state builder, and assertion helpers. Tests go in internal/api/admin_integration_test.go.4---56# td-sync Admin API Integration Tests78Write integration tests in `internal/api/admin_integration_test.go` using the harness in `internal/api/testharness_test.go`. Tests run against a real HTTP server on a random port with isolated temp databases.910## Quick Start Pattern1112```go13func TestIntegration_DescriptiveName(t *testing.T) {14 t.Parallel()15 h := newTestHarness(t)16 state := h.Build().17 WithUser("user@test.com").18 WithAdmin("admin@test.com", "admin:read:server,sync").19 WithProject("proj1", "user@test.com").20 WithEvents("proj1", "user@test.com", 5).21 Done()2223 token := state.AdminToken("admin@test.com")24 pid := state.ProjectID("proj1")2526 var resp adminEventsResponse27 h.DoJSON("GET", fmt.Sprintf("/v1/admin/projects/%s/events", pid), token, nil, &resp)2829 if len(resp.Data) != 5 {30 t.Fatalf("expected 5 events, got %d", len(resp.Data))31 }32}33```3435## Harness API3637See [references/harness-api.md](references/harness-api.md) for the complete API reference with all method signatures and detailed usage notes.3839### Core4041- `newTestHarness(t, ...func(*Config)) *TestHarness` -- real HTTP server, isolated DB, auto-cleanup42- `h.Do(method, path, token, body) *http.Response` -- real HTTP request (caller closes body)43- `h.DoJSON(method, path, token, body, &out) *http.Response` -- request + JSON decode (fatals on 4xx/5xx)4445### State Builder4647```go48h.Build().49 WithUser(email). // sync-scoped key50 WithAdmin(email, scopes). // admin key with scopes51 WithProject(name, ownerEmail). // via API (owner must exist)52 WithMember(projectName, email, role). // "owner"/"writer"/"reader"53 WithEvents(projectName, userEmail, count).// cycles issues/logs/comments54 WithSnapshot(projectName). // triggers snapshot build55 WithAuthEvents(count). // inserts directly to DB56 WithRateLimitEvents(count). // inserts directly to DB57 Done() // -> *TestState58```5960Ordering matters: create users before projects, projects before members/events/snapshots.6162### State Accessors6364`state.UserToken(email)`, `state.UserID(email)`, `state.AdminToken(email)`, `state.ProjectID(name)`, `state.Harness()`6566### Assertions6768- `AssertStatus(t, resp, 200)` -- checks status, prints body on failure69- `AssertErrorResponse(t, resp, 403, "insufficient_admin_scope")` -- checks status + error code70- `ReadJSON[T](t, resp) T` -- generic JSON decode71- `AssertPaginated[T](t, resp, count, hasMore) PaginatedResponse[T]` -- checks paginated list72- `AssertCORSHeaders(t, resp, origin)` / `AssertNoCORSHeaders(t, resp)`73- `h.AssertRequiresAdminScope(t, method, path, wrongToken)` -- 403 + error code check7475## Admin Scopes7677| Scope | Endpoints |78|-------|-----------|79| `admin:read:server` | server/overview, server/config, rate-limit-violations, users, users/{id}, users/{id}/keys, auth/events |80| `admin:read:projects` | projects, projects/{id}, projects/{id}/members, sync/status, sync/cursors |81| `admin:read:events` | projects/{id}/events, projects/{id}/events/{seq}, entity-types |82| `admin:read:snapshots` | projects/{id}/snapshot/meta, projects/{id}/snapshot/query |83| `admin:export` | projects/{id}/events/export |8485## Response Types8687Internal types accessible from test files in package `api`:8889- `serverOverviewResponse` -- server overview90- `serverConfigResponse` -- server config91- `adminEventsResponse` -- `{Data []adminEvent, HasMore bool}`92- `adminEvent` -- single event: `ServerSeq`, `EntityType`, `EntityID`, `ActionType`, `Payload`93- `adminSyncStatusResponse` -- `{EventCount, LastServerSeq, LastEventTime}`94- `adminCursorEntry` -- `{ClientID, LastEventID, LastSyncAt, DistanceFromHead}`95- `serverdb.AdminProject` -- project: `ID, Name, MemberCount, EventCount`96- `serverdb.AdminUser` -- user: `ID, Email, IsAdmin, ProjectCount`97- `serverdb.AdminProjectMember` -- `{UserID, Email, Role}`9899## Rules1001011. Always `t.Parallel()` -- each harness is isolated1022. Test name prefix: `TestIntegration_`1033. Config overrides via opts: `newTestHarness(t, func(cfg *Config) { cfg.CORSAllowedOrigins = []string{"https://x.com"} })`1044. First user created is auto-admin; consume with `h.CreateUser("first@test.com")` when testing non-admin denial1055. CORS tests need manual `http.NewRequest` since `Do` doesn't support custom headers1066. Run tests: `go test -v -run TestIntegration ./internal/api/`