# Td Integration Test

> 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.

- Skill: `marcus/td-integration-test` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add marcus/td-integration-test`
- Raw SKILL.md: https://api.skillmd.com/api/skills/marcus/td-integration-test/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: marcus (https://skillmd.com/u/marcus)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/marcus/td-integration-test

---


# 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/`

