Skip to main content

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

LayerTool
UnitVitest + @nestjs/testing
IntegrationVitest + Testcontainers (Postgres 16 + Redis 7 + Pub/Sub emulator) + WireMock
ContractPact JS
E2EPlaywright + dedicated bff-tenant-booking-e2e runner against stage
Loadk6
SecurityOWASP ZAP, pnpm audit, gitleaks, Trivy, Cosign
MutationStryker (nightly on critical files)

3. Unit tests

3.1 Critical files (100% line + branch)

FileNotes
src/application/use-cases/handle-payment-return.use-case.tsIdempotent re-entry, replay safety
src/application/use-cases/create-payment-intent.use-case.tsState machine transitions, optimistic concurrency
src/application/use-cases/create-hold.use-case.tsQuote→hold linkage, idempotency
src/application/use-cases/verify-and-consume-handoff.use-case.tsHMAC, replay, tenant match
src/domain/state-machine.tsAll transitions enumerated; no illegal forward path possible
src/security/handoff-verifier.tsConstant-time compare, key rotation, version
src/cache/single-flight.tsRace + leader/follower correctness
src/composers/availability-vm.tsPartial-result composition
src/composers/confirmation-vm.tsDefensive defaults when upstream missing
src/orchestrators/compose-tenant-bootstrap.tsCache key correctness, version skew

3.2 Property-based tests (fast-check)

  • BookingDraft state-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) raises CROSS_TENANT_REFERENCE.

4. Integration tests

Run against ephemeral Postgres + Redis + Pub/Sub emulator + WireMock upstreams.

4.1 Mandatory (gating CI)

TestAsserts
tenant-isolation.spec.tsCross-tenant draft access blocked at controller, application, RLS
outbox.spec.tsDomain events written transactionally with state; relay publishes once-and-only-once-effectively
inbox.spec.tsCache-invalidation events deduplicated by message ID

4.2 Functional

ScenarioAsserts
Bootstrap coldComposes correctly; cache populated; bootstrap.served.v1 enqueued
Bootstrap with handoff tokenhandoffPayload returned; handoff_arrival_log row written
Bootstrap stale (X-Bootstrap-Version >7d old)410 + BOOTSTRAP_STALE
Search → availability → quote → holdSingle happy path; draft state machine progresses correctly
Hold with quote-expired410 + MELMASTOON.PRICING.QUOTE_EXPIRED
Hold with overbooking409 + MELMASTOON.RESERVATION.OVERBOOKING_BLOCKED
Patch draft with conflicting expectedUpdatedAt412 + DRAFT_CONFLICT
Payment intent + return successfulReservation confirmed; draft.converted.v1 enqueued
Payment return idempotent re-entrySame outcome; one draft.converted.v1 only
Payment return with declined provider402 + MELMASTOON.PAYMENT.DECLINED; flow → failed
Payment return with hold expired409; flow → failed
Cash-on-arrival flowpaymentIntent.method='cash_on_arrival'; no redirect; /confirm direct
Confirmation viewComposes 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-flowBootstrap returns theme.isRtl=false; CSS sheet swap
Unknown tenant slug404 + SLUG_UNKNOWN
Suspended tenant503 + MELMASTOON.TENANT.SUSPENDED
Handoff replaysecond consume → 409 + HANDOFF_REPLAYED
Handoff signature invalid401 + HANDOFF_SIGNATURE_INVALID
Handoff tenant mismatch422 + HANDOFF_TENANT_MISMATCH
Handoff expired410 + HANDOFF_EXPIRED
Memorystore downBest-effort ephemeral session; mutating endpoints 503 + MELMASTOON.BFF.CACHE_UNAVAILABLE
Pub/Sub downOutbox grows; recovers when Pub/Sub healthy
Sweep abandoned draftsTTL'd drafts → draft.abandoned.v1; cold mirror inserted
Single-flight under concurrent identical bootstrapOne upstream call; followers receive same payload

4.3 Idempotency tests

  • POST /quote with same X-Idempotency-Key returns identical body across 5 concurrent calls.
  • POST /hold with same key creates one upstream reservation hold.
  • POST /draft/{id}/payment-intent with same key returns same intent ID.
  • POST /draft/{id}/return is replay-safe across 10 concurrent calls.
  • Different bodies under same key → 412 MELMASTOON.GENERAL.PRECONDITION_FAILED.

4.4 PII tests

  • telemetry-pii.spec.ts asserts no raw email/phone/name in any outbox row.
  • audit-log-pii.spec.ts asserts 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's POST /internal/handoff/{id}/consume consumer pact (cross-BFF handoff handshake).
  • @ghasi/app-web-tenant-booking and @ghasi/app-mobile-tenant-booking consumer pacts for bootstrap, 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:

FlowSteps
Anonymous booking — happy pathBootstrap → search → select room → quote → hold → guest details → payment (stub) → return → confirmation
Booking with handoff tokenURL with ?h=<token> → bootstrap returns handoffPayload → directly to selecting → continues
Booking abandonmentHold → wait 31 min → draft.abandoned.v1 observed in Pub/Sub
Cash-on-arrivalSame path with method=cash_on_arrival; no redirect; direct confirm
RTL → LTR locale switch mid-flowBootstrap re-fetched; CSS swap; flow continues without state loss
Currency switch mid-flowRe-quote; new totals; flow continues
Custom-domain (canary tenant)Same booking against https://booking.canary-test.com
Concurrent payment-return retries5 parallel returns → exactly one confirmation
Handoff replaySecond consume of same token → user banner + new bootstrap

E2E test users / tenants pre-provisioned via Terraform (stage-canary-1stage-canary-3).

7. Load tests (k6)

7.1 Profiles

ProfileRPSDurationMix
Steady-state150 RPS15 min30% bootstrap, 30% availability, 15% quote, 10% hold, 10% return, 5% confirmation
Flash sale0 → 1200 RPS over 2 min, hold 10 min12 min25% bootstrap, 40% availability, 20% quote, 10% hold, 5% return
Booking burst0 → 500 RPS confirm-heavy5 min60% return + confirm
Long soak80 RPS8 hmixed

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:

ExperimentExpected 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 outageCircuit 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 failoverMutating endpoints return 503 for ≤ 60 s; reads unaffected
Pub/Sub publish errors 100%Outbox grows; redrives when Pub/Sub recovers
Theme service slowBootstrap serves cached version; banner if > 5 min stale
HMAC key rotation drillRotate 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: gitleaks on every PR.
  • Image scan: Trivy.
  • HMAC fuzzing: fast-check property + handcrafted tampered tokens; expected 100% reject.
  • CSRF tests: mutating endpoints reject when Origin header is missing or off allow-list.
  • Cookie tests: tnt_id always HttpOnly; 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:

  1. pnpm install --frozen-lockfile
  2. pnpm lint && pnpm typecheck
  3. pnpm test:unit (parallel)
  4. pnpm test:integration (Testcontainers Postgres + Redis + Pub/Sub emulator)
  5. Pact consumer publish + verify upstream pacts
  6. pnpm audit && gitleaks detect
  7. Build + Trivy + Cosign sign
  8. Deploy to dev Cloud Run; smoke test
  9. (e2e label) — 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