SKILL.md
td-sync Admin API Integration Tests
Write integration tests in internal/api/adminintegrationtest.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("[email protected]").
WithAdmin("[email protected]", "admin:read:server,sync").
WithProject("proj1", "[email protected]").
WithEvents("proj1", "[email protected]", 5).
Done()
token := state.AdminToken("[email protected]")
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](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, "insufficientadminscope") -- 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("[email protected]") 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/