td-integration-test
Installation
SKILL.md
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-cleanuph.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 failureAssertErrorResponse(t, resp, 403, "insufficient_admin_scope")-- checks status + error codeReadJSON[T](t, resp) T-- generic JSON decodeAssertPaginated[T](t, resp, count, hasMore) PaginatedResponse[T]-- checks paginated listAssertCORSHeaders(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 overviewserverConfigResponse-- server configadminEventsResponse--{Data []adminEvent, HasMore bool}adminEvent-- single event:ServerSeq,EntityType,EntityID,ActionType,PayloadadminSyncStatusResponse--{EventCount, LastServerSeq, LastEventTime}adminCursorEntry--{ClientID, LastEventID, LastSyncAt, DistanceFromHead}serverdb.AdminProject-- project:ID, Name, MemberCount, EventCountserverdb.AdminUser-- user:ID, Email, IsAdmin, ProjectCountserverdb.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.NewRequestsinceDodoesn't support custom headers - Run tests:
go test -v -run TestIntegration ./internal/api/
Related skills