Skip to main content

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)

LayerStatementsBranchesFunctionsLines
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

TierToolingWhat it testsCount target
Unit (domain)VitestAggregates, value objects, invariants, state machines, contrast checker, bundle builder≥ 600
Unit (application)Vitest with port doublesUse cases — happy paths, OCC, idempotency, RBAC, validation≥ 250
Integration (DB)Vitest + Testcontainers PostgresRepositories, RLS isolation, materialised view refresh, OCC race≥ 80
Integration (infra adapters)Vitest + emulatorsGCS, Pub/Sub, Memorystore, CDN client adapters≥ 40
Integration (HTTP)Vitest + supertest + TestcontainersControllers end-to-end against real DB + emulated infra≥ 90
Contract (API)spectral + SchemathesisOpenAPI conformance + property-based fuzzspec-derived
Contract (events)ajv against JSON-Schemas + fixturesProducer + consumer compatper-event fixtures
Eval (AI)Custom harnessPrompt eval suites in AI_INTEGRATION §7nightly
E2EPlaywright via the booking-flow E2E suiteBooking flow renders correctly under brand A vs B; rollback restorescurated
Loadk6Publish under concurrent edits; bundle reads at edgequarterly + pre-launch
Chaosplatform GameDayMemorystore down, Pub/Sub backpressure, GCS slowquarterly

3. Mandatory unit tests (domain)

File under testRequired scenarios
Theme.aggregate.tsconstruction validation, addLocale, removeLocale (with LocaleInUseError), setActivePublication, OCC bumps version, soft-delete
ThemeVersion.aggregate.tsstate-machine transitions; rejected illegal transitions; applyPatch emits diff; immutability when published/archived; AI provenance preservation
DesignTokenSet.vo.tsevery ColorTokens foreground/background pair WCAG AA enforcement; spacing logical-property derivation; motion bounds; direction validation
ContrastChecker.tsreference test vectors from W3C; passesAA for normal vs large text
bundle.builder.tsbyte-identical output for the same input; canonical key sort; deterministic gzip; SHA-256 stability across Node minor versions
BookingFlowConfig.aggregate.tsstep ordinal uniqueness; allow-list of step IDs; consent block invariants
EmailTheme.aggregate.tssafe font list; email-safe contrast pairs
LocalePack.aggregate.tsICU placeholder parity vs default-locale source pack
NavigationConfig.aggregate.tsdepth ≤ 3; ordinal uniqueness; route-allow-list; mailto/tel/external rules
PreviewToken.aggregate.tsTTL 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

EndpointCoverage
POST /themes201 success; 403 unauthorized; 409 duplicate scope; 400 validation
PATCH /themes/:id200 success; 412 stale If-Match; 409 locale-in-use
POST /themes/:id/versions201 success; 403 unauthorized
PATCH /theme-versions/:id200 success; 409 immutable; 412 stale; 400 validation; AI provenance preserved
POST /theme-versions/:id/preview201 success; preview URL never leaks the secret on subsequent reads; revoked on draft edit
POST /theme-versions/:id/publish202 success; 409 conflict; 403 HITL required; 400 validation; idempotency replay
POST /themes/:id/rollback202 success; 400 invalid target; 409 conflict
GET /public/themes/:id/published.json200 with proper Cache-Control + ETag; 304 on If-None-Match
GET /public/preview/:token200 with watermark; 401 invalid token; 410 expired token; constant-time compare
GET /internal/email-theme/:id200 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:

  1. Loads the dataset.
  2. Calls the orchestrator (or a local cassette in CI).
  3. Asserts pass criteria.
  4. 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 to ar-SA flips 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 by ProvisionDefaultThemeUseCase tests.
  • 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 for ps-AF, fa-AF, en-US, ar-SA with 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