TESTING_STRATEGY — bff-consumer-service
Sibling: APPLICATION_LOGIC · API_CONTRACTS · SECURITY_MODEL · FAILURE_MODES
1. Test pyramid
┌──────────────┐
│ E2E (3%) │ ← real upstream stage env
├──────────────┤
│ Contract (7%)│ ← Pact: BFF↔upstream + BFF↔BFF (handoff)
├──────────────┤
│ Integration│ ← NestJS Test app + ephemeral Postgres + Memorystore
│ (25%) │
├──────────────┤
│ Unit (65%) │ ← orchestrators, BotDetector, HandoffSigner, schema validators
└──────────────┘
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) + nock for HTTP fakes |
| Contract | Pact JS (consumer + provider) |
| E2E | Playwright + dedicated bff-consumer-e2e test runner against stage |
| Load | k6 + dedicated load-test project + scripted realistic traffic mix |
| Security | OWASP ZAP (DAST), pnpm audit, Trivy (image), Cosign (signing) |
| Mutation | Stryker (nightly on critical files: HandoffSigner, BotDetector, orchestrators) |
3. Unit tests
3.1 Coverage targets per file (critical)
| File | Target |
|---|---|
src/orchestrators/execute-search-list-view.ts | 100% lines |
src/orchestrators/read-hotel-detail.ts | 100% lines |
src/orchestrators/mint-booking-handoff.ts | 100% lines |
src/orchestrators/consume-booking-handoff.ts | 100% lines |
src/orchestrators/add-to-wishlist.ts | 100% lines |
src/security/handoff-signer.ts | 100% lines + property-based tests |
src/security/bot-detector.ts | 100% lines |
src/composers/listing-card.ts | 100% lines |
src/composers/hotel-detail-vm.ts | 100% lines |
src/cache/single-flight.ts | 100% lines + race tests |
3.2 Property-based tests (fast-check)
- HMAC sign/verify roundtrip for any valid
BookingHandoffpayload — never tampers, never accepts mutated payload. - Canonical signing string is byte-stable across orderings of input object keys.
cacheKeyderivation is collision-free for distinct(query, locale, currency)tuples.- BotDetector verdict is monotone in score.
4. Integration tests
Run against ephemeral Postgres + Redis containers via Testcontainers.
4.1 Scenarios
| Scenario | Asserts |
|---|---|
| Bootstrap session for first-time visitor | gms_ cookie set; Memorystore key present; session.started.v1 enqueued in outbox |
| Search with cold cache | Single-flight leader path; cache populated; follower returns same payload |
| Search with hot cache | No upstream HTTP call; Cloud Trace span has cache.singleflight.outcome=hit |
Search with degraded pricing-service (timeout 50% of fanouts) | Returns full results; missing prices marked priceFromCheapest=null; MELMASTOON.BFF.UPSTREAM_PARTIAL annotation in response meta |
Hotel detail with stale light-availability | Best-effort placeholder; warning header present |
| Wishlist add → list → remove | Postgres + Memorystore stay in sync; second-add idempotent |
| Handoff mint → upstream consume | consumed=true; second consume returns already-consumed outcome |
| Handoff mint with suspended tenant | 422 + MELMASTOON.BFF.CONSUMER.HANDOFF_TENANT_SUSPENDED |
Currency override X-Currency: ZZZ | 422 + MELMASTOON.COMMON.VALIDATION_FAILED |
| Locale fallback chain | Accept-Language: ps-AF, ar-SA;q=0.5 selects ps-AF then ar-SA then en-US |
| Bot pre-screen (curl UA + cadence > 30 rpm) | 429 + MELMASTOON.BFF.CONSUMER.BOT_SUSPECTED; bot_suspected.v1 queued |
| reCAPTCHA challenge satisfaction | 200 returned after X-Recaptcha-Token accepted |
| Memorystore down | Returns 503 + MELMASTOON.BFF.CACHE_UNAVAILABLE; circuit opens |
| Postgres outbox enqueue retried | Pub/Sub publisher retries from outbox after Pub/Sub downtime |
4.2 Idempotency tests
- Same
Idempotency-KeyforPOST /handoffreturns identicalhandoffTokenandhandoffIdacross 5 concurrent retries. - Same key for
POST /wishlistreturns the samewishlistItemIdand emitswishlist.added.v1exactly once. - Different bodies under the same key are rejected with 409.
5. Contract tests (Pact)
5.1 As consumer
This BFF is a Pact consumer of:
search-aggregation-service(search, search/pins, light-availability, facets, popularity)pricing-service(POST /quotes/preview)property-service(detail bundle endpoints)theme-config-service(/brand-peek/batch)tenant-service(suspended-tenant lookup)
For each consumer pact, we publish to the Pact Broker and gate provider releases on the broker's verification report.
5.2 As provider
This BFF is a Pact provider for:
bff-tenant-booking-service'sPOST /internal/handoff/{id}/consumeconsumer pact (cross-BFF handoff handshake).
We verify against pacts published by the tenant BFF on every PR.
6. E2E tests (stage)
Run nightly + on every release candidate:
| Flow | Steps |
|---|---|
| Anonymous search → handoff | Cold session bootstrap; search Kabul; click first result; mint handoff; redirect URL resolves; tenant BFF accepts and renders bootstrap |
| Wishlist persistence across sessions | Add → cookie clear → restore from access token (Phase 2 only) |
| Bot challenge satisfied | Headless detection triggers reCAPTCHA; mocked verifier returns success; second request succeeds |
| Handoff replay rejected | Mint → consume → consume again → tenant BFF returns HANDOFF_REPLAYED |
| Currency switch | Switch from AFN to USD on detail page; price refreshes; currency.changed.v1 emitted |
E2E uses Playwright with real Cloud Run stage targets and synthetic guest sessions. Test users / tenants are pre-provisioned via Terraform.
7. Load tests (k6)
7.1 Profiles
| Profile | RPS | Duration | Mix |
|---|---|---|---|
| Steady-state | 200 RPS | 15 min | 70% search, 15% hotel detail, 5% handoff, 5% wishlist, 5% telemetry |
| Campaign spike | 0 → 1500 RPS over 2 min, hold 10 min | 12 min | 90% search list, 5% search detail, 5% handoff |
| Bot wave | 50 RPS legitimate + 200 RPS scripted-bot pattern | 10 min | Asserts ≥ 95% of bot pattern is rejected with 429 |
| Long-soak | 100 RPS | 8 h | Memory + connection-leak detection |
7.2 Targets
- Steady-state: p95 < 700 ms; error rate < 0.1%; instance count converges < 8.
- Campaign spike: p95 < 900 ms after warm-up; error rate < 0.5%; cache hit ratio > 90%.
- Bot wave: legitimate p95 unaffected (within 10%); bot pattern rejection rate > 95%.
- 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 | /search p95 still < 1500 ms (parallel fanout absorbs slow requests; cheapest-rate marked null after 800 ms cap) |
search-aggregation-service 30 s outage | Circuit opens; cached responses served until expiry; new searches return 503 + MELMASTOON.BFF.UPSTREAM_UNAVAILABLE |
| Memorystore failover (replica promoted) | < 30 s of elevated latency; no errors after failover; sessions preserved |
| Postgres failover | Mutating endpoints return 503 for ≤ 60 s; reads unaffected (no Postgres on hot read path) |
| Pub/Sub publish errors 100% | Outbox grows; redrive job consumes when Pub/Sub recovers; no event loss |
| Cosign signature verification disabled | Deploy is rejected by binary authorization (must remain enabled) |
9. Security tests
- DAST nightly: OWASP ZAP scan against stage; baseline + active scan; HTML/JSON report archived.
- Per-PR static: ESLint +
@typescript-eslint/strict+eslint-plugin-security+pnpm audit. - Secrets scan:
gitleakson every PR. - Image scan: Trivy on every container build; high/critical CVE blocks promotion to prod.
- HMAC fuzzing: Property-based tests + handcrafted tampered tokens; expected: 100% reject.
- CSRF tests: Verify mutating endpoints reject when
Originheader is missing or off-allow-list. - Cookie tests: Verify
gms_cookie carriesHttpOnly,Secure,SameSite=Lax.
10. Test data
- Tenants: 5 fixed seed tenants in
stage(kabul-grand-hotel,herat-bazaar-inn,mazar-pearl,jalalabad-orchard,bamiyan-cliff). - Properties: 12 per tenant.
- Currencies: 3 (AFN, USD, EUR).
- Locales: 3 (
ps-AF,fa-AF,en-US).
Test fixtures live in services/bff-consumer-service/test/fixtures/ and are regenerated quarterly from the seed environment.
11. CI pipeline
Every PR:
pnpm install --frozen-lockfilepnpm lint && pnpm typecheckpnpm test:unit(Vitest, parallel)pnpm test:integration(Testcontainers Postgres + Redis)- Pact consumer publish + verify upstream pacts
pnpm audit && gitleaks detect- Build + Trivy scan + Cosign sign
- Push to dev artifact registry; deploy to dev Cloud Run; smoke test
- (PR label
e2e) — run subset of Playwright E2E against dev
Nightly:
- Stryker mutation tests on critical files (must achieve mutation score ≥ 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 + tenant-BFF consumer pact.
- DAST report has no high/critical findings.