Files
galaxy-game/TESTING.md
T
2026-05-06 10:14:55 +03:00

211 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TESTING.md
Test strategy for the [Galaxy Game](ARCHITECTURE.md) platform after the
consolidation that moved every domain concern into `galaxy/backend`.
The platform now ships three executables — `gateway`, `backend`,
`game` (the engine container) — plus the shared `pkg/*` libraries.
This document defines the layering of tests, the responsibilities of
each layer, and the mandatory minimum coverage per executable.
## Three layers
1. **Service tests** verify a single executable in isolation. They
live next to the implementation as `*_test.go` files and use only
in-process or testcontainers-managed dependencies.
2. **Inter-service integration tests** verify one cross-process seam
between two real executables (most often `gateway ↔ backend`,
sometimes `backend ↔ game`). They live in
[`integration/`](integration/) and drive the platform from outside
the trust boundary.
3. **Full system tests** are a small, focused subset of the
integration suite that walks an entire user-facing flow from the
client edge through every component the flow touches. They live in
the same `integration/` module and reuse the same fixtures.
Service tests are the cheapest and the broadest; integration tests
are slower and broader; full-system tests are the slowest and the
narrowest. The pyramid stays in this order — never replace a service
test with a system test.
## Global rules
- Every executable owns the service tests for its packages. Adding a
new package without `_test.go` files is a review block.
- Every cross-process seam must have at least one passing
inter-service test before the seam is wired in production.
- Async flows (mail outbox, notification routes, runtime workers,
push gRPC) get tests for both the success path and the retry /
dead-letter path, and a duplicate-event safety check.
- Sync flows get happy path, validation failure, timeout
propagation, and dependency unavailable.
- Every external or trusted-internal API must have contract tests
alongside behaviour tests. `backend/internal/server/contract_test.go`
is the reference; gateway runs the same shape against
`gateway/openapi.yaml`.
- The integration suite must keep running on a developer machine
with Docker available; tests skip cleanly with a clear message
when the daemon is unreachable.
## Service-specific coverage
### `galaxy/gateway`
Service tests live under `gateway/internal/`:
- Public REST routing, error projection, and OpenAPI contract
validation.
- Authenticated gRPC envelope verification (`grpcapi.Server`):
signature, payload hash, freshness window, anti-replay reservation,
unknown / revoked sessions.
- Session cache (`session.BackendCache`) — the only implementation
in the codebase, a thin wrapper around the `backendclient.RESTClient`
per-request lookup.
- Response signing for unary responses and stream events
(`authn.ResponseSigner`).
- Push hub (`push.Hub`) and push fan-out (`push_fanout.go`).
- Replay store (`replay.RedisStore`) reservation semantics.
- Anti-abuse rate limits per IP / session / user / message class.
### `galaxy/backend`
Service tests live under `backend/internal/`:
- Startup wiring: `app.App` lifecycle, telemetry runtime, Postgres
pool, embedded migrations.
- OpenAPI contract test (`internal/server/contract_test.go`):
validates every documented operation against the live gin engine.
- Domain unit + e2e tests per package (`auth`, `user`, `admin`,
`lobby`, `runtime`, `mail`, `notification`, `geo`, `push`).
E2E tests (`*_e2e_test.go`) spin up a Postgres testcontainer.
- Mail outbox: pickup with `SELECT FOR UPDATE SKIP LOCKED`, retry
with backoff plus jitter, dead-letter past `MAX_ATTEMPTS`,
resend semantics (`pending|retrying|dead_lettered` → re-armed,
`sent` → 409).
- Notification: idempotent `Submit`, route materialisation, push +
email fan-out, `OnUserDeleted` cascade.
- Lobby: state-machine transitions, RND canonicalisation, sweeper.
- Runtime: per-game mutex serialisation, worker pool, scheduler,
reconciler, force-next-turn skip flag.
- Admin: bcrypt cost 12, idempotent bootstrap, write-through cache,
409 Conflict on duplicate username, last-used timestamp.
- Geo: counter increment on every authenticated request,
declared-country write at registration, fail-open semantics.
### `galaxy/game`
The engine has its own service tests under `game/`:
- OpenAPI contract test (`game/openapi_contract_test.go`).
- Engine lifecycle (init, status, turn, banish, command, order,
report) implemented by the engine package suites.
## Integration test coverage (`integration/`)
The integration module is the single home for inter-service and
full-system tests. Every scenario calls `testenv.Bootstrap(t)` which
brings up Postgres, Redis, mailpit, the backend image, the gateway
image, and (when needed) the engine image.
Mandatory inter-service coverage:
- **Gateway ↔ Backend (public auth)**:
`auth_flow_test.go` — register + confirm with mailpit-captured
code; declared_country populated; idempotent re-confirm.
- **Gateway ↔ Backend (authenticated user surface)**:
`user_account_test.go`, `user_profile_update_test.go`,
`user_settings_update_test.go` — signed envelope, FlatBuffers
payload, response signature verification, BCP 47 / IANA validation.
- **Gateway ↔ Backend (anti-replay, signature, freshness)**:
`gateway_edge_test.go` — body-too-large, bad signature,
payload_hash mismatch, stale timestamp, unknown session,
unsupported `protocol_version`.
- **Gateway ↔ Backend (push)**:
`notification_flow_test.go`, `session_revoke_test.go` — push
delivery to a SubscribeEvents stream and immediate stream close
on revoke.
- **Gateway ↔ Backend (anti-replay)**:
`anti_replay_test.go` — duplicate `request_id` rejected.
- **Backend ↔ Postgres** is exercised by every backend e2e test
through testcontainers; integration tests do not duplicate it.
- **Backend ↔ SMTP**:
`mail_flow_test.go` — login-code email captured by mailpit; admin
list reaches `sent`; resend on `sent` returns 409.
- **Backend ↔ Game engine**:
`runtime_lifecycle_test.go`, `engine_command_proxy_test.go`
start container, healthz green, command, force-next-turn, finish,
race name promotion.
- **Admin surface (REST)**:
`admin_flow_test.go`, `admin_global_games_view_test.go`,
`admin_engine_versions_test.go`, `admin_user_sanction_test.go`
bootstrap + CRUD; visibility split between user and admin queries;
engine-version registry CRUD; permanent block cascade.
- **Lobby flow without engine**:
`lobby_flow_test.go` — owner-creates-private-game →
open-enrollment → invite → redeem → memberships listing.
- **Soft delete cascade**:
`soft_delete_test.go``POST /api/v1/user/account/delete`
cascades through auth/lobby/notification/geo, gateway rejects
subsequent calls.
- **Geo counters**:
`geo_counter_increments_test.go` — multiple authenticated
requests with different `X-Forwarded-For` values increment the
user's per-country counter rows.
Full-system flows beyond the inter-service set are intentionally
limited; pick scenarios that exercise the longest vertical slice
the platform supports today.
## Out-of-scope (legacy architecture)
The previous nine-service architecture defined components that no
longer exist as distinct services. Their behaviour either lives
inside `backend` (and is therefore covered by backend service or
integration tests) or has been removed:
- *Auth/Session Service*, *User Service*, *Notification Service*,
*Mail Service*, *Game Lobby Service*, *Runtime Manager*,
*Game Master*, *Admin Service* — consolidated into
`backend/internal/*`. Inter-service seams between these former
services are now in-process function calls; they are exercised by
backend service tests, not by integration tests.
- *Geo Profile Service* (suspicious-multi-country detection,
review-recommended state, session blocking through geo) — not
implemented. The geo concern is intentionally minimal (see
`ARCHITECTURE.md §10`) and the test plan does not assert on
features we do not ship.
- *Billing Service* — not implemented; no tests required until it
appears.
## Practical execution
During day-to-day development:
- Run `go test ./<service>/...` for the service you are touching;
this is fast (Postgres testcontainers add ~35 s per package that
uses them).
- Run `go test ./integration/...` before opening a PR that touches a
cross-process seam. Cold runs build three Docker images
(`galaxy/backend:integration`, `galaxy/gateway:integration`,
`galaxy/game:integration`) — budget ~3 min for the cold path,
~75 s for the warm path.
- CI runs every layer on every push. Integration tests skip with a
clear message if Docker is not available.
## Adding a new test
1. Decide the layer: service, inter-service, or system. A backend
change usually lands as service tests plus an integration test
for any new cross-process behaviour.
2. Reuse `testenv` fixtures rather than rolling your own
container orchestration.
3. Follow the bootstrap-per-test pattern; do not share a global
stack across tests.
4. Make the test deterministic: explicit timeouts (no
`time.Sleep`), `t.Logf` instead of `fmt.Println`, no
`t.Parallel()` in `integration/`.
5. Adding a new service-test file is fine; adding an
integration-test file requires that the seam be reachable
through gateway's REST or gRPC surface (or through backend HTTP
directly with `X-User-ID` for routes that gateway does not yet
register).