TESTING_STRATEGY — bff-tenant-booking-service
Sibling: APPLICATION_LOGIC · API_CONTRACTS · SECURITY_MODEL · FAILURE_MODES
1. Test pyramid
┌──────────────┐
│ E2E (4%) │ ← Playwright stage; full booking happy + error paths
├──────────────┤
│ Contract (8%)│ ← Pact: BFF↔upstream + BFF↔BFF (handoff)
├──────────────┤
│ Integration │ ← NestJS Test app + ephemeral Postgres + Redis + WireMock
│ (28%) │
├──────────────┤
│ Unit (60%) │ ← orchestrators, state machine, HandoffVerifier, idempotency, caches
└──────────────┘
Coverage gates (Vitest c8):
- Statements ≥ 90%
- Branches ≥ 85%
- Functions ≥ 90%
- Lines ≥ 90%
2. Tooling
| Layer | Tool |
|---|---|
| Unit | Vitest + @nestjs/testing |
| Integration | Vitest + Testcontainers (Postgres 16 + Redis 7 + Pub/Sub emulator) + WireMock |
| Contract | Pact JS |
| E2E | Playwright + dedicated bff-tenant-booking-e2e runner against stage |
| Load | k6 |
| Security | OWASP ZAP, pnpm audit, gitleaks, Trivy, Cosign |
| Mutation | Stryker (nightly on critical files) |
3. Unit tests
3.1 Critical files (100% line + branch)
| File | Notes |
|---|---|
src/application/use-cases/handle-payment-return.use-case.ts | Idempotent re-entry, replay safety |
src/application/use-cases/create-payment-intent.use-case.ts | State machine transitions, optimistic concurrency |
src/application/use-cases/create-hold.use-case.ts | Quote→hold linkage, idempotency |
src/application/use-cases/verify-and-consume-handoff.use-case.ts | HMAC, replay, tenant match |
src/domain/state-machine.ts | All transitions enumerated; no illegal forward path possible |
src/security/handoff-verifier.ts | Constant-time compare, key rotation, version |
src/cache/single-flight.ts | Race + leader/follower correctness |
src/composers/availability-vm.ts | Partial-result composition |
src/composers/confirmation-vm.ts | Defensive defaults when upstream missing |
src/orchestrators/compose-tenant-bootstrap.ts | Cache key correctness, version skew |
3.2 Property-based tests (fast-check)
BookingDraftstate-machine: any random sequence of allowed transitions ends in a reachable state.- HMAC sign/verify roundtrip via shared library — no payload mutation accepted.
- Cache-key derivation collision-free for distinct
(tenantId, propertyId, dateRange, currency)tuples. - Idempotency: same
(idemKey, requestHash)→ same response on N concurrent calls. - Tenant guard: any cross-tenant
(tenantA token, tenantB resource)raisesCROSS_TENANT_REFERENCE.
4. Integration tests
Run against ephemeral Postgres + Redis + Pub/Sub emulator + WireMock upstreams.
4.1 Mandatory (gating CI)
| Test | Asserts |
|---|---|
tenant-isolation.spec.ts | Cross-tenant draft access blocked at controller, application, RLS |
outbox.spec.ts | Domain events written transactionally with state; relay publishes once-and-only-once-effectively |
inbox.spec.ts | Cache-invalidation events deduplicated by message ID |
4.2 Functional
| Scenario | Asserts |
|---|---|
| Bootstrap cold | Composes correctly; cache populated; bootstrap.served.v1 enqueued |
| Bootstrap with handoff token | handoffPayload returned; handoff_arrival_log row written |
Bootstrap stale (X-Bootstrap-Version >7d old) | 410 + BOOTSTRAP_STALE |
| Search → availability → quote → hold | Single happy path; draft state machine progresses correctly |
| Hold with quote-expired | 410 + MELMASTOON.PRICING.QUOTE_EXPIRED |
| Hold with overbooking | 409 + MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED |
Patch draft with conflicting expectedUpdatedAt | 412 + DRAFT_CONFLICT |
| Payment intent + return successful | Reservation confirmed; draft.converted.v1 enqueued |
| Payment return idempotent re-entry | Same outcome; one draft.converted.v1 only |
| Payment return with declined provider | 402 + MELMASTOON.PAYMENT.DECLINED; flow → failed |
| Payment return with hold expired | 409; flow → failed |
| Cash-on-arrival flow | paymentIntent.method='cash_on_arrival'; no redirect; /confirm direct |
| Confirmation view | Composes from reservation + folio + lock placeholder |
| Currency change mid-flow (AFN→USD) | Re-quote on next quote; currency.changed.v1 enqueued |
| Locale change mid-flow (ps-AF→en-US) | Bootstrap re-fetched if version differs; locale.changed.v1 enqueued |
| Locale RTL→LTR mid-flow | Bootstrap returns theme.isRtl=false; CSS sheet swap |
| Unknown tenant slug | 404 + SLUG_UNKNOWN |
| Suspended tenant | 503 + MELMASTOON.TENANT.SUSPENDED |
| Handoff replay | second consume → 409 + HANDOFF_REPLAYED |
| Handoff signature invalid | 401 + HANDOFF_SIGNATURE_INVALID |
| Handoff tenant mismatch | 422 + HANDOFF_TENANT_MISMATCH |
| Handoff expired | 410 + HANDOFF_EXPIRED |
| Memorystore down | Best-effort ephemeral session; mutating endpoints 503 + MELMASTOON.BFF.CACHE_UNAVAILABLE |
| Pub/Sub down | Outbox grows; recovers when Pub/Sub healthy |
| Sweep abandoned drafts | TTL'd drafts → draft.abandoned.v1; cold mirror inserted |
| Single-flight under concurrent identical bootstrap | One upstream call; followers receive same payload |
4.3 Idempotency tests
POST /quotewith sameX-Idempotency-Keyreturns identical body across 5 concurrent calls.POST /holdwith same key creates one upstream reservation hold.POST /draft/{id}/payment-intentwith same key returns same intent ID.POST /draft/{id}/returnis replay-safe across 10 concurrent calls.- Different bodies under same key → 412
MELMASTOON.GENERAL.PRECONDITION_FAILED.
4.4 PII tests
telemetry-pii.spec.tsasserts no raw email/phone/name in any outbox row.audit-log-pii.spec.tsasserts the same for Cloud Logging structured payloads.
5. Contract tests (Pact)
5.1 As consumer
This BFF is a Pact consumer of:
tenant-service(slug resolution, suspension)theme-config-service(theme bundle, flow config, policies)property-service(property + room types + photos)inventory-service(availability per date)pricing-service(cheapest rate, quote)reservation-service(hold, confirm, reservation read)payment-gateway-service(intent, return verification)billing-service(folio summary)lock-integration-service(key-credential placeholder)ai-orchestrator-service(recommendation, policy summary)
For each consumer pact, we publish to the Pact Broker; provider releases gate on broker verification.
5.2 As provider
This BFF is a Pact provider for:
bff-consumer-service'sPOST /internal/handoff/{id}/consumeconsumer pact (cross-BFF handoff handshake).@ghasi/app-web-tenant-bookingand@ghasi/app-mobile-tenant-bookingconsumer pacts forbootstrap,availability,quote,hold,draft,payment-intent,return,confirmation.
We verify against client pacts on every PR.
6. E2E tests (stage)
Run nightly + on every release candidate via Playwright + tenant-booking-canary harness:
| Flow | Steps |
|---|---|
| Anonymous booking — happy path | Bootstrap → search → select room → quote → hold → guest details → payment (stub) → return → confirmation |
| Booking with handoff token | URL with ?h=<token> → bootstrap returns handoffPayload → directly to selecting → continues |
| Booking abandonment | Hold → wait 31 min → draft.abandoned.v1 observed in Pub/Sub |
| Cash-on-arrival | Same path with method=cash_on_arrival; no redirect; direct confirm |
| RTL → LTR locale switch mid-flow | Bootstrap re-fetched; CSS swap; flow continues without state loss |
| Currency switch mid-flow | Re-quote; new totals; flow continues |
| Custom-domain (canary tenant) | Same booking against https://booking.canary-test.com |
| Concurrent payment-return retries | 5 parallel returns → exactly one confirmation |
| Handoff replay | Second consume of same token → user banner + new bootstrap |
E2E test users / tenants pre-provisioned via Terraform (stage-canary-1 … stage-canary-3).
7. Load tests (k6)
7.1 Profiles
| Profile | RPS | Duration | Mix |
|---|---|---|---|
| Steady-state | 150 RPS | 15 min | 30% bootstrap, 30% availability, 15% quote, 10% hold, 10% return, 5% confirmation |
| Flash sale | 0 → 1200 RPS over 2 min, hold 10 min | 12 min | 25% bootstrap, 40% availability, 20% quote, 10% hold, 5% return |
| Booking burst | 0 → 500 RPS confirm-heavy | 5 min | 60% return + confirm |
| Long soak | 80 RPS | 8 h | mixed |
7.2 Targets
- Steady-state: p95 < 600 ms; error rate < 0.1%; instance count converges < 8.
- Flash sale: p95 < 1 s; error rate < 0.5%; cache hit > 90% on bootstrap.
- Booking burst: confirm p95 < 1.5 s; reservation confirms succeed > 99%.
- Long soak: no memory growth > 10% over baseline; no connection leaks.
8. Chaos tests
Quarterly in stage:
| Experiment | Expected behaviour |
|---|---|
pricing-service 500 ms latency injection | /availability p95 still < 1.5 s; cheapest fanout marked stale after 800 ms cap |
reservation-service 30 s outage | Circuit opens; /hold and /confirm return 503 + MELMASTOON.BFF.UPSTREAM_UNAVAILABLE; users see banner |
payment-gateway-service outage | /payment-intent returns 502; users see retry option; in-flight /return gracefully fails with MELMASTOON.PAYMENT.GATEWAY_TIMEOUT |
| Memorystore failover | < 30 s elevated latency; sessions preserved; drafts not lost (snapshot on transition) |
| Postgres failover | Mutating endpoints return 503 for ≤ 60 s; reads unaffected |
| Pub/Sub publish errors 100% | Outbox grows; redrives when Pub/Sub recovers |
| Theme service slow | Bootstrap serves cached version; banner if > 5 min stale |
| HMAC key rotation drill | Rotate current → new key; verify both work for 7 days; remove old |
9. Security tests
- DAST nightly: OWASP ZAP scan against stage.
- Per-PR static: ESLint +
eslint-plugin-security+@typescript-eslint/strict+pnpm audit. - Secrets scan:
gitleakson every PR. - Image scan: Trivy.
- HMAC fuzzing: fast-check property + handcrafted tampered tokens; expected 100% reject.
- CSRF tests: mutating endpoints reject when
Originheader is missing or off allow-list. - Cookie tests:
tnt_idalwaysHttpOnly; Secure; SameSite=Lax. - CSP nonce uniqueness: 1000 concurrent requests → 1000 distinct nonces.
- Tenant-isolation tests: any cross-tenant attempt blocked at every layer.
10. Test data
- 5 stage tenants (
kabul-grand-hotel,herat-bazaar-inn,mazar-pearl,jalalabad-orchard,bamiyan-cliff); 12 properties each. - 3 currencies (AFN, USD, EUR).
- 3 locales (
ps-AF,fa-AF,en-US). - 4 payment methods stubbed in
payment-gateway-stub(card, paypal, mfs, cash_on_arrival). - Test fixtures in
services/bff-tenant-booking-service/test/fixtures/; regenerated quarterly.
11. CI pipeline
Per PR:
pnpm install --frozen-lockfilepnpm lint && pnpm typecheckpnpm test:unit(parallel)pnpm test:integration(Testcontainers Postgres + Redis + Pub/Sub emulator)- Pact consumer publish + verify upstream pacts
pnpm audit && gitleaks detect- Build + Trivy + Cosign sign
- Deploy to dev Cloud Run; smoke test
- (
e2elabel) — subset of Playwright against dev
Nightly:
- Stryker mutation tests on critical files (≥ 75%)
- ZAP DAST against stage
- Long soak load test
- Chaos experiments (rotating)
12. Release readiness checklist (linked from SERVICE_READINESS)
- All P1 alerts have ack'd runbooks
- p95/p99 SLOs met in load test
- HMAC key rotation drill executed in stage in last 90 days
- Mutation score ≥ 75% on critical files
- Pact verifications green for all upstreams + bff-consumer-service consumer pact
- DAST report has no high/critical findings
- Tenant isolation tests pass