TESTING_STRATEGY — theme-config-service
Sibling: APPLICATION_LOGIC · DOMAIN_MODEL · SERVICE_READINESS
Platform anchors:
docs/standards/SERVICE_TEMPLATE.md·docs/standards/DEFINITION_OF_DONE.md
This document describes the test pyramid, mandatory test categories, fixtures, and CI gating for theme-config-service. The service is mission-critical to the booking flow's first impression; we hold it to a high coverage and behavioural-test bar.
1. Coverage targets (CI-enforced)
| Layer | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
src/domain/** | ≥ 95 % | ≥ 95 % | ≥ 95 % | ≥ 95 % |
src/application/** | ≥ 90 % | ≥ 90 % | ≥ 90 % | ≥ 90 % |
src/infrastructure/** | ≥ 80 % | ≥ 75 % | ≥ 80 % | ≥ 80 % |
src/interface/** (controllers) | ≥ 85 % | ≥ 80 % | ≥ 85 % | ≥ 85 % |
CI fails any PR that drops coverage below targets or by more than 1 % from main.
2. Test pyramid
| Tier | Tooling | What it tests | Count target |
|---|---|---|---|
| Unit (domain) | Vitest | Aggregates, value objects, invariants, state machines, contrast checker, bundle builder | ≥ 600 |
| Unit (application) | Vitest with port doubles | Use cases — happy paths, OCC, idempotency, RBAC, validation | ≥ 250 |
| Integration (DB) | Vitest + Testcontainers Postgres | Repositories, RLS isolation, materialised view refresh, OCC race | ≥ 80 |
| Integration (infra adapters) | Vitest + emulators | GCS, Pub/Sub, Memorystore, CDN client adapters | ≥ 40 |
| Integration (HTTP) | Vitest + supertest + Testcontainers | Controllers end-to-end against real DB + emulated infra | ≥ 90 |
| Contract (API) | spectral + Schemathesis | OpenAPI conformance + property-based fuzz | spec-derived |
| Contract (events) | ajv against JSON-Schemas + fixtures | Producer + consumer compat | per-event fixtures |
| Eval (AI) | Custom harness | Prompt eval suites in AI_INTEGRATION §7 | nightly |
| E2E | Playwright via the booking-flow E2E suite | Booking flow renders correctly under brand A vs B; rollback restores | curated |
| Load | k6 | Publish under concurrent edits; bundle reads at edge | quarterly + pre-launch |
| Chaos | platform GameDay | Memorystore down, Pub/Sub backpressure, GCS slow | quarterly |
3. Mandatory unit tests (domain)
| File under test | Required scenarios |
|---|---|
Theme.aggregate.ts | construction validation, addLocale, removeLocale (with LocaleInUseError), setActivePublication, OCC bumps version, soft-delete |
ThemeVersion.aggregate.ts | state-machine transitions; rejected illegal transitions; applyPatch emits diff; immutability when published/archived; AI provenance preservation |
DesignTokenSet.vo.ts | every ColorTokens foreground/background pair WCAG AA enforcement; spacing logical-property derivation; motion bounds; direction validation |
ContrastChecker.ts | reference test vectors from W3C; passesAA for normal vs large text |
bundle.builder.ts | byte-identical output for the same input; canonical key sort; deterministic gzip; SHA-256 stability across Node minor versions |
BookingFlowConfig.aggregate.ts | step ordinal uniqueness; allow-list of step IDs; consent block invariants |
EmailTheme.aggregate.ts | safe font list; email-safe contrast pairs |
LocalePack.aggregate.ts | ICU placeholder parity vs default-locale source pack |
NavigationConfig.aggregate.ts | depth ≤ 3; ordinal uniqueness; route-allow-list; mailto/tel/external rules |
PreviewToken.aggregate.ts | TTL bounds; revocation idempotency |
Each scenario asserts both the success outcome and the specific error code on failure (no generic throws Error matchers).
4. Mandatory integration tests
4.1 RLS isolation (integration/rls/cross_tenant_isolation.spec.ts)
it('hides themes from another tenant', async () => {
await asTenant('tnt_a', () => themesRepo.save(themeA));
const visible = await asTenant('tnt_b', () => themesRepo.list('tnt_b' as TenantId));
expect(visible.find(t => t.id === themeA.id)).toBeUndefined();
});
Variants for every table that carries tenant_id. Required by the platform DoD.
4.2 OCC race (integration/concurrency/occ_publish_race.spec.ts)
Two concurrent publish attempts on the same theme: exactly one must win with a 202; the other must receive 409 MELMASTOON.THEME.PUBLISH_CONFLICT. The single-active-publication invariant must hold afterwards.
4.3 Publish atomicity (integration/publish/publish_atomicity.spec.ts)
Force the GCS upload to succeed but the publication-flip transaction to fail (mid-TX trigger): assert no orphan in theme_publications, version remains in prior state, GCS object becomes orphan and is reaped by lifecycle.
4.4 Outbox / inbox (integration/eventing/outbox.spec.ts, inbox.spec.ts)
Outbox: writes inside the same TX as the aggregate save; partial commit drops both. Drain produces Pub/Sub publishes in created_at order; failures retried with backoff.
Inbox: duplicate eventId is short-circuited; consumed events idempotent.
4.5 Bundle integrity (integration/cdn/bundle_integrity.spec.ts)
Build → upload → fetch back → recompute SHA-256 must match. Mutate the GCS object out-of-band → next read raises theme.bundle.integrity.violation and the monitoring counter increments.
4.6 Materialised view refresh (integration/views/published_theme_view.spec.ts)
Publish → theme.published.v1 consumed by view-refresh worker → view reflects new publication within 5 s.
5. Mandatory HTTP / controller tests
| Endpoint | Coverage |
|---|---|
POST /themes | 201 success; 403 unauthorized; 409 duplicate scope; 400 validation |
PATCH /themes/:id | 200 success; 412 stale If-Match; 409 locale-in-use |
POST /themes/:id/versions | 201 success; 403 unauthorized |
PATCH /theme-versions/:id | 200 success; 409 immutable; 412 stale; 400 validation; AI provenance preserved |
POST /theme-versions/:id/preview | 201 success; preview URL never leaks the secret on subsequent reads; revoked on draft edit |
POST /theme-versions/:id/publish | 202 success; 409 conflict; 403 HITL required; 400 validation; idempotency replay |
POST /themes/:id/rollback | 202 success; 400 invalid target; 409 conflict |
GET /public/themes/:id/published.json | 200 with proper Cache-Control + ETag; 304 on If-None-Match |
GET /public/preview/:token | 200 with watermark; 401 invalid token; 410 expired token; constant-time compare |
GET /internal/email-theme/:id | 200 mTLS-only; 401 if not service principal |
Property-based fuzz via Schemathesis runs nightly against staging and on PR for any change to controllers.
6. AI eval harness
Per AI_INTEGRATION §7, evals live under services/theme-config-service/evals/. Each eval is a Vitest describe.each that:
- Loads the dataset.
- Calls the orchestrator (or a local cassette in CI).
- Asserts pass criteria.
- Emits a JSON report into
evals/reports/<date>/<suite>.json.
CI gates:
- Any change under
prompts/triggers the relevant eval suite; failure blocks merge. - Nightly job runs all suites; regressions open a Slack ticket and a JIRA item.
7. E2E tests (booking-flow integration)
The E2E suite is owned by the BFF/booking-flow team but includes brand-driven scenarios that exercise this service:
brand_publish_propagates.spec.ts— publish on backoffice; within 60 s the booking-flow renders new tokens.rollback_restores.spec.ts— rollback to N-1; tokens revert; CSS variables match.ar_locale_rtl.spec.ts— switching toar-SAflips layout direction; spacing logical properties applied; no text clipping.preview_link.spec.ts— preview URL renders new tokens with watermark; subsequent draft edit invalidates the preview.
These are tagged @theme-config so the team can run them on PRs that touch this service.
8. Load tests
tests/load/k6/:
publish_burst.k6.js: 50 concurrent publish attempts across 5 tenants for 10 min; SLOs assert no rejected publishes, p95 e2e ≤ 4 s.bundle_read_edge.k6.js: 5k rps to the public bundle URL across 10 hosts; SLOs assert p95 ≤ 80 ms, error rate ≤ 0.01 %.authoring_mixed.k6.js: realistic backoffice mix (70 % read, 25 % patch, 5 % publish) at 200 actors; SLOs assert p95 PATCH ≤ 350 ms.
Run quarterly + before any major architectural change; results archived in tests/load/reports/.
9. Chaos / GameDay scenarios
- Memorystore failover: validate degraded-mode reads (origin GCS) keep p95 ≤ 250 ms.
- Pub/Sub publish backpressure: outbox grows, alert fires, no data loss when backpressure clears.
- GCS regional outage: publish flow fails fast with 503; no inconsistent state.
- AI orchestrator timeout: all AI surfaces return 503 with
Retry-After; no long requests; no data loss. - Concurrent publish + tenant.config_updated handler: assert both succeed; final bundle reflects new currency.
10. Test data & fixtures
test/fixtures/scaffolds/— JSON snapshots of platform default theme scaffold; loaded byProvisionDefaultThemeUseCasetests.test/fixtures/bundles/— golden bundles for byte-identity assertions.test/fixtures/events/— JSON-Schema fixtures for every event (producer + consumer test).test/fixtures/locales/— sample LocalePacks forps-AF,fa-AF,en-US,ar-SAwith edge-case ICU placeholders.test/builders/— fluent builders (aTheme().withTenant(...).buildPersisted()) keep tests readable.
Fixtures are checked-in (no random generation in unit tests) so failures are reproducible.
11. Pre-merge CI workflow
lint → type-check → unit → contract (openapi, events) → integration → http → evals (changed prompts)
│
└→ coverage gate
Total pre-merge time budget: ≤ 10 min on the standard runner (parallelised across shards). The full eval suite runs nightly only.
12. References
- DoD:
docs/standards/DEFINITION_OF_DONE.md - Test scaffolding template:
docs/standards/SERVICE_TEMPLATE.md - AI evals:
AI_INTEGRATION §7 - RLS contract:
SECURITY_MODEL §4