Documentation
¶
Overview ¶
Package contract holds the golden contract corpus for the public HTTP API (PRD #112, issue #116) and the machinery that makes it executable: a canonicalizer, a semantic JSON differ, and the recorded request battery.
The corpus freezes the observable behavior of the current gRPC + gateway stack — status codes, contract-relevant headers, and response bodies — so the stdlib net/http rewrite can be driven red→green against it, and so it can live on permanently as the contract regression net.
Comparison is semantic JSON equality: parse → canonicalize → diff. Byte equality is explicitly NOT the contract (protojson randomizes whitespace per build, so today's API is already not byte-stable).
Exactly one transform is applied between the wire and the goldens: stripKeycloakID. See its doc comment.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CompareGolden ¶
CompareGolden semantically compares a committed golden against an observed one. Returned strings are human-readable mismatches; empty means equal.
Besides status/headers/body it self-checks the recorded request metadata (case, method, path, auth) so a golden replayed under the wrong request is a diff rather than silently write-only, flags recorded header keys outside the contractHeaders allowlist, and rejects malformed goldens that carry both or neither of body/bodyText.
func SaveGolden ¶
SaveGolden writes g to its file under dir, creating directories as needed. The file itself is deterministic: fixed field order, canonical body.
Types ¶
type Auth ¶
type Auth string
Auth identifies which credential a battery case presents. The concrete header values live in authHeader; goldens record the tier, not the token.
const ( // AuthNone sends no Authorization header. AuthNone Auth = "none" // AuthRawKey sends a bare key without the "Bearer " scheme. AuthRawKey Auth = "raw-key" // AuthInvalidKey sends a well-formed Bearer token that no key matches. AuthInvalidKey Auth = "invalid-key" // AuthRead is a valid key holding the "read" scope (milpacs surface). AuthRead Auth = "read" // AuthReadTickets is a valid key holding the "read:tickets" scope. AuthReadTickets Auth = "read:tickets" // AuthNoScopes is a valid key holding no scopes at all. AuthNoScopes Auth = "no-scopes" )
type Case ¶
type Case struct {
Name string
// Method is always GET today; kept explicit so the corpus stays honest
// if a non-GET route ever appears.
Method string
// Path is the request target including any query string, exactly as a
// client would send it (URL-encoded).
Path string
Auth Auth
// Notes documents why the case exists (shows up in the golden file).
Notes string
}
Case is one recorded request in the battery. Name doubles as the golden file path relative to the goldens dir (slashes create directories).
func Cases ¶
func Cases() []Case
Cases returns the full recorded request battery: every surviving public route × happy/edge/error/auth cases, both enum path forms, repeated-filter and dual-spelling query combinations on tickets.
17 public routes (the keycloak lookup route /api/v1/milpac/keycloak/{keycloak_id} is deliberately NOT recorded — it dies at cutover, so there is no golden to hold it to):
- GET /api/v1/milpacs/profile/id/{user_id}
- GET /api/v1/milpacs/profile/username/{username}
- GET /api/v1/milpac/discord/{discord_id}
- GET /api/v1/milpac/gamertag/{gamertag}
- GET /api/v1/roster/{roster}
- GET /api/v1/roster/{roster}/lite
- GET /api/v1/s1/uniforms/{roster}
- GET /api/v1/milpacs/position/search/{position_query=**}
- GET /api/v1/milpacs/ranks
- GET /api/v1/milpacs/position/groups
- GET /api/v1/milpacs/awol
- GET /api/v1/tickets
- GET /api/v1/tickets/{ticket_id}
- GET /api/v1/tickets/ref/{ticket_ref}
- GET /api/v1/tickets/{ticket_id}/messages
- GET /api/v1/tickets/categories
- GET /api/v1/forum/groups
Path literals reference the recording seed (see fake_datastore_test.go and contract/README.md): relation 1 ↔ user 3 (Jarvis.A), relation 2 ↔ user 8 (John.Doe), tickets 42/43/44, injected-outage ids 777 (profile) and ROSTER_TYPE_ARLINGTON (roster). Cursor literals are base64url of the deterministic seed cursors, exactly as the stack emits them.
type Golden ¶
type Golden struct {
Case string `json:"case"`
Method string `json:"method"`
Path string `json:"path"`
Auth Auth `json:"auth"`
Notes string `json:"notes,omitempty"`
Status int `json:"status"`
Header map[string]string `json:"header"`
// Body holds the canonical JSON body; null when the body is not JSON.
Body json.RawMessage `json:"body,omitempty"`
// BodyText holds the verbatim body when it is not JSON.
BodyText *string `json:"bodyText,omitempty"`
}
Golden is the recorded contract for one case: status, allowlisted headers, and the response body. JSON bodies are stored canonicalized (sorted keys, stable whitespace, keycloakId stripped); non-JSON bodies (the plain-text 401 tier) are stored verbatim in BodyText.
func LoadGolden ¶
LoadGolden reads the committed golden for a case name and validates its structural invariants: exactly one of body/bodyText set and a defined auth tier — a hand-edited golden violating either is an error, not a silent pass.