`time.LoadLocation` is called from
backend/internal/server/handlers_public_auth.go:108 (confirm-email-code)
and backend/internal/user/account.go:218 (user.settings.update). Both
runtime images shipped today have no tzdata — production
backend/Dockerfile uses gcr.io/distroless/static-debian12:nonroot, and
local-dev tools/local-dev/backend.Dockerfile uses alpine:3.20 without
the optional tzdata apk — so the container-side binary resolves only
the no-data fallback (UTC and fixed offsets) and rejects every real
IANA zone with HTTP 400 `invalid_request: time_zone must be a valid
IANA zone`.
Adding `import _ "time/tzdata"` to backend's main is the idiomatic
Go fix: the binary embeds the IANA database, time.LoadLocation works
on every base image, no Dockerfile changes needed. Cost is ~800 KB
of binary growth — invisible next to the existing /usr/local/bin/backend
size and well below any container layer threshold.
The OpenAPI spec already documents the field as "IANA time-zone
identifier" (gateway/openapi.yaml:205, backend/openapi.yaml:2334)
and the UI sends Intl.DateTimeFormat().resolvedOptions().timeZone,
so neither the contract nor the client needs a change.
Why this slipped through: backend unit tests run as a host Go test
process (developer's tzdata covers them), Playwright tests mock the
gateway (backend never reached), and the integration suite — the only
layer that exercises the real backend container — uses
RegisterSession which hardcoded `time_zone="UTC"`. Switching that
default to "Europe/Berlin" makes every integration scenario that
enrols a pilot exercise the tzdata path, so the next regression
surfaces in the integration run instead of escaping into manual
smoke. (The integration suite is not in the per-PR workflow yet; that
gap is tracked separately.)
Verified end-to-end against `tools/local-dev`:
- Europe/Amsterdam, Asia/Tokyo, America/Los_Angeles → 200 +
device_session_id (was 400 before this patch).
- Mars/Olympus still → 400 (validation behaviour unchanged).
Host tests: backend/internal/{auth,user,config} green.
UI: pnpm test 14/14, CI=1 pnpm exec playwright test 44/44.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
integration
End-to-end test suite for the Galaxy platform. The suite drives gateway
from outside and verifies behaviour at the public boundary while
backend and galaxy/game run as Docker containers managed by the
test process via testcontainers-go.
For cross-cutting testing principles (unit vs integration boundaries,
why testcontainers tests pin no-op observability providers, why
infrastructure failures in this suite fail loudly instead of skipping)
see docs/TESTING.md. This README focuses on
the integration-specific runbook: prerequisites, entry points,
labels, and per-test fixtures.
Prerequisites
- A reachable Docker daemon (
DOCKER_HOSTor the local socket). - Go toolchain matching the workspace
go.workdirective. - Network access for the first run (
postgres:16-alpine,axllent/mailpit,redis:7-alpineimages are pulled). Subsequent runs reuse the local image cache.
Run
The recommended entry points are the Makefile targets:
make -C integration preclean # idempotent leftover cleanup
make -C integration integration # preclean + serial test run
make -C integration integration-step # preclean + one-test-at-a-time
preclean removes stale containers and locally-built images from
earlier runs; it never touches testcontainers-pulled service images
(postgres:16-alpine, axllent/mailpit, redis:7-alpine,
testcontainers/ryuk), so the cache stays warm. The cleanup keys
off labels:
org.testcontainers=true— every container/network created bytestcontainers-go(our backend/gateway/game and the postgres / redis / mailpit / ryuk service containers).galaxy.backend=1— engine instances spawned by backend's runtime adapter directly on the host Docker daemon (seebackend/internal/dockerclient/types.go).galaxy.test.kind=integration-image— local builds ofgalaxy/{backend,gateway,game}:integrationproduced bytestenv/images.go.
integration runs every test in the module sequentially
(-p=1 -parallel=1) — recommended default on a slow / shared Docker.
integration-step runs them one at a time with a fresh preclean
before each test and stops on the first failure; useful to isolate a
flake or build up to a full pass without losing context to subsequent
tests.
Direct go test ./integration/... still works but does not pre-clean
or serialise the suite; use it only on a hand-cleaned Docker.
The suite builds three Docker images on demand from the workspace sources:
galaxy/backend:integration(backend/Dockerfile),galaxy/gateway:integration(gateway/Dockerfile),galaxy/game:integration(game/Dockerfile).
Each image is built once per go test invocation, guarded by a
sync.Once inside testenv, and stamped with the
galaxy.test.kind=integration-image label so preclean can wipe it
on the next run. The first cold run is slow (~2–3 min on a
developer machine); subsequent runs reuse the layer cache.
Skipping
Tests skip with a clear message when the Docker daemon is unreachable.
Subsuites that require a live engine container (lobby_flow_test.go)
also skip when the galaxy/game image cannot be built.
Layout
testenv/— fixtures: Postgres, Redis, mailpit, GeoLite2 mmdb, image builders, backend/gateway runners, signed gRPC client (built on top of the publicgalaxy/gateway/authnpackage, no duplicated canonical-bytes code), mailpit HTTP client,EnrollPilotshelper for runtime-driven scenarios that need ≥10 members, platform bootstrap.*_test.go— one file per cross-service scenario.
The runtime-driven tests (runtime_lifecycle_test.go,
engine_command_proxy_test.go) honour the engine's production
contract len(races) >= 10: each registers ten extra pilots with
synthetic Player01..Player10 race names and matching emails, has
the owner invite each one, and has each pilot redeem the invite
before admin force-start. Cold runs add ~30 s for the ten extra
mailpit round-trips on top of the engine image build.
Determinism
- Each test calls
Bootstrap(t)to spin up a dedicated Postgres, Redis, mailpit, backend and gateway. Cross-test contamination is not possible. - Tests do not call
t.Parallel(). Docker resource pressure makes parallel suites flaky on commodity hardware. - Gateway anti-abuse and body-size limits are loosened for the bulk of
scenarios (so legitimate flows are not rate-limited mid-test) and
intentionally tightened in
gateway_edge_test.goso each protective mechanism can be observed firing.