td-integration-test · git:20260209.6af8885 · 2026-02-09 · sha256 02e667b33712a043
td-integration-test git:20260209.6af8885A
Immutable. This exact content never changes and is served at /api/v1/blob/02e667b33712a043.
---
name: td-integration-test
description: 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.
---
# 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
```go
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](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
```go
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
1. Always `t.Parallel()` -- each harness is isolated
2. Test name prefix: `TestIntegration_`
3. Config overrides via opts: `newTestHarness(t, func(cfg *Config) { cfg.CORSAllowedOrigins = []string{"https://x.com"} })`
4. First user created is auto-admin; consume with `h.CreateUser("first@test.com")` when testing non-admin denial
5. CORS tests need manual `http.NewRequest` since `Do` doesn't support custom headers
6. Run tests: `go test -v -run TestIntegration ./internal/api/`