Skip to main content

TESTING_STRATEGY — reservation-service

Sibling: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS

Strategic anchors: 02 §15 Testing · standards/SERVICE_TEMPLATE · standards/DEFINITION_OF_DONE

The booking saga is the most consequential multi-service flow on the platform. Testing for reservation-service is shaped by three forces: domain correctness (state machine + invariants), saga correctness (forward + every compensation), and offline-correctness (desktop-replicated subset never diverges). Coverage targets follow the platform DOD — overall ≥ 85%, domain layer ≥ 95% — but this service additionally must ship the per-saga compensation matrix and the FX-snapshot stability test before production.


1. Test pyramid

LayerFrameworkTargetsWhere
Unit (domain)Vitest≥ 95% statements; 100% branches on state machinesrc/domain/__tests__/, test/unit/domain/
Unit (application)Vitest≥ 90% statements per use casetest/unit/application/
Integration (in-service)Vitest + Testcontainers (Postgres + Pub/Sub emulator)≥ 80% pathstest/integration/
Contract (consumer)Pact (broker)every produced + consumed event has a contracttest/contract/
API contractOpenAPI snapshot diff + dreddevery endpointCI gate
End-to-endPlaywright via bff-backoffice-service and bff-tenant-booking-servicetop 5 user journeystest/e2e/
Chaos / low-bandwidthk6 + custom toxiproxy harnesslatency, packet loss, disconnecttest/chaos/

The three mandatory integration tests (tenant-isolation.spec.ts, outbox.spec.ts, inbox.spec.ts) gate the readiness checklist and must be present before any feature work — see SERVICE_TEMPLATE §Mandatory tests.


2. Unit — domain

SpecCoverage
reservation.state-machine.spec.tsevery legal transition (table-driven from DOMAIN_MODEL §3.1); every illegal transition rejected with ILLEGAL_TRANSITION
reservation.invariants.spec.tsone test per invariant I1..I14 (pass + fail)
cancellation-policy-evaluator.spec.tsboundary tests per policy snapshot version (e.g., tenant.cancellation_policy@v1, @v2, @v3); refund/penalty math; OCC-safe
overstay-detector.spec.tsproperty time-zone (Asia/Kabul, Asia/Tehran, Asia/Dushanbe) + DST transitions
modification-differ.spec.tssymmetric, minimal diffs; nested arrays in items and additionalGuests; ordered tag arrays do not produce churn
stay-window-overlap-detector.spec.ts1-night, 30-night, contiguous, overlapping, exclusive-end semantics
fx-snapshot.spec.tsimmutability post-confirm; rate precision; IRR scale (large numbers)
reservation.code-generator.spec.tsdeterministic, collision behavior under simulated duplicates
occupancy-validator.spec.tsI9 ±1 tolerance; child-without-id flagged but allowed

The domain test suite imports nothing outside @ghasi/domain-primitives and Vitest. CI runs lint:layer-boundaries to enforce.


3. Unit — application

SpecWhat it asserts
request-quote.use-case.spec.tscalls PricingClient, emits quote.created.v1, idempotent on Idempotency-Key
hold-reservation.use-case.spec.tsquote-expiry rejection; inventory hold success; payment intent created; held.v1 outboxed
confirm-reservation.use-case.spec.tsevent-driven; replays are no-ops; FX snapshot locked
cancel-reservation.use-case.spec.tspolicy evaluation; OCC; refund/penalty; cancelled.v1 outboxed
modify-reservation.*.use-case.spec.tsone spec per sub-type; sub-saga step ordering; failure handling
check-in.use-case.spec.ts + check-out.use-case.spec.tsearly/normal; folio open/close request; lock client called; requiresManualKey path
walk-in.use-case.spec.tscombined create+check-in; deferKyc flag; idempotency per operator session
merge-reservations.use-case.spec.ts, split-group.use-case.spec.tsguards and audit rows
record-no-show.use-case.spec.ts, record-early-checkout.use-case.spec.tspolicy applied; events emitted
add-special-request.use-case.spec.tsAI parser fallback; provenance persisted
expire-holds.use-case.spec.tsbatch behavior; idempotency on already-expired aggregates

Mocks for ports use the in-memory fakes shipped under __fakes__/ (no Sinon/Mockito-style spies for use-case logic — fakes assert behavior).


4. Integration — in-service

Run against ephemeral Postgres + Pub/Sub emulator under Testcontainers. Each test spins up a clean database via Drizzle migrations.

4.1 Mandatory three (gate the readiness checklist)

  • test/integration/tenant-isolation.spec.ts — proves cross-tenant reads return 404, cross-tenant writes raise 403/MELMASTOON.TENANT.MISMATCH, RLS holds even with raw SQL bypass attempt.
  • test/integration/outbox.spec.ts — proves aggregate save + outbox row are atomic; relay publishes at-least-once; mid-batch crash leaves no event lost or double-committed.
  • test/integration/inbox.spec.ts — proves consumed events deduped by event_id; idempotent on retry; consumer crash mid-handle resumes correctly.

4.2 Booking saga happy path + 8 compensation paths (this service must own these)

Forward and every named compensation. Each test plants events into the Pub/Sub emulator and asserts the resulting aggregate state, audit, and emitted events.

#ScenarioAsserted state
H1Forward path: hold → inventory.committed → payment.captured → confirmed → key.issued (today)confirmed, keyCredentialIds.length=1, no manual key
C1inventory.allocation.failed.v1reservation cancelled with reasonCode=inventory_failed; pending payment intent cancelled
C2payment.transaction.failed.v1cancelled/reasonCode=payment_failed; inventory release event emitted
C3Hold TTL elapsed without confirmexpired_hold; inventory release; payment intent cancellation
C4Confirm-then-cancel during 24h policy windowcancelled, refund event for full amount, key revoke event emitted (no key yet so no-op)
C5Confirm-then-cancel beyond policy windowcancelled, partial refund per tenant.cancellation_policy@vN, audit row with policy ref
C6lock.key.failed.v1 at check-inreservation transitions to checked_in with requiresManualKey=true; alert event emitted
C7Date-change sub-saga: payment delta charge failsinventory reallocation reverted; reservation unchanged; MELMASTOON.PAYMENT.CHARGE_FAILED returned
C8Operator modifies reservation while saga in flight (pendingSagaStep != null)command rejected with MELMASTOON.RESERVATION.SAGA_IN_FLIGHT

4.3 Concurrency

  • test/integration/concurrency-check-in.spec.ts — two concurrent check-in attempts; one succeeds, the other receives STALE_VERSION; no double key issuance.
  • test/integration/concurrency-cancel-vs-modify.spec.ts — cancel and date-change submitted simultaneously; one wins; modification audit reflects only the winner.
  • test/integration/concurrency-group-partial-cancel.spec.ts — three items, two operators each cancelling one; both succeed; final cancellation across all items flips reservation to cancelled.
  • test/integration/hold-expiry-race.spec.ts — payment.captured arrives at the same instant as hold-expiry sweeper; outcome must be deterministic (sweeper checks payment_status and pending_saga_step and skips).

4.4 Date-arithmetic edge cases

  • DST transitions in Asia/Tehran, Asia/Kabul (no DST since 2022 but historical data may exist), Asia/Dushanbe.
  • Year-boundary stays (2026-12-30..2027-01-02).
  • Single-night vs zero-night attempts (zero rejected with INVALID_STAY_WINDOW).
  • Long stays (90+ nights — extended-stay tenants).
  • Property local-date conversions: a stay starting on 2026-04-25 for a property at +03:30 should not be misclassified as starting 2026-04-24 UTC.

4.5 FX snapshot stability

  • test/integration/fx-snapshot-stability.spec.ts: confirm a USD-priced reservation for an IRR tenant. Subsequently mutate the tenant's pricing-service USD/IRR rate. Re-read the reservation; assert the persisted fxSnapshot is unchanged. Modify the reservation (special_request_added); assert FX snapshot still unchanged. Attempt direct overwrite via raw SQL outside the aggregate factory; assert the next aggregate save raises MELMASTOON.RESERVATION.FX_SNAPSHOT_LOCKED.
  • IRR magnitude test: confirm a 2-night stay totalling 23,478,000,000,000 micro-IRR; assert no overflow in bigint serialization or BigQuery sink.

4.6 Cash-on-arrival flow

  • test/integration/cash-on-arrival.spec.ts: hold → confirm with paymentMethod=cash_on_arrival (no captured payment yet) → assert payment.status=pending_cash, reservation in confirmed, no payment intent capture event consumed yet → simulate cash event via billing-service → reservation unchanged in state, payment.status advances to captured.

5. Contract tests

  • Pact (consumer): for every event produced (reservation.*), a Pact contract under test/contract/ describes the schema; consumers (inventory-service, billing-service, lock-integration-service, notification-service, analytics-service, audit-service, search-aggregation-service) verify against the broker.
  • Pact (provider): for every event consumed (payment.*, inventory.*, lock.*, tenant.*, property.*), a contract verifies our handlers behave per shared expectations.
  • OpenAPI: the controller-derived openapi.json is diffed against the previous release; breaking changes block the PR (must ship /api/v2).
  • Event schema registry: every produced subject is validated against the JSON Schema in EVENT_SCHEMAS; CI fails on drift.

6. End-to-end

Top user journeys, run nightly via Playwright in a staging environment with seeded data:

  1. Direct booking, card — funnel from quote to confirm to email arrival.
  2. Walk-in cash-on-arrival — front-desk operator captures + checks in within 90 s.
  3. Modify dates with delta charge — confirm + extend by 1 night + new key validity in TTLock simulator.
  4. Cancel with partial refund — policy-applied refund visible on folio + cancellation email.
  5. Group partial cancel — one item of three cancelled; reservation remains confirmed.

Each journey asserts both the BFF response and the resulting events on the Pub/Sub emulator.


7. Chaos / low-bandwidth

  • Toxiproxy between bff-backoffice-service and reservation-service: introduce 800 ms latency, 5% packet loss, periodic 30-second blackouts. Walk-in flow must complete or queue without data loss.
  • Pub/Sub emulator restart mid-batch: outbox relay must resume without double-publish or loss.
  • Postgres failover drill: read traffic rerouted during 30 s primary unavailability; writes return 503 with retry hint; OCC re-checks succeed on resume.
  • Lock vendor outage simulation: lock-integration-service returns 503 for 10 minutes; check-ins complete with requiresManualKey=true; alarm RESV-007 fires.

8. Performance targets (load tests)

  • 200 concurrent quote requests / 100 concurrent holds per tenant: p99 latency stays under 1.5 s (quote) / 2.5 s (hold).
  • 1,000 reservations/day per large tenant: outbox lag stays < 30 s p99.
  • Hold-expiry sweeper handles a 10,000-stale-hold backlog in a single 60-s pass without timeout.

k6 scripts live under test/perf/ and are run weekly in staging.


9. Test data builders

Builders are colocated under src/domain/__builders__/ and test/__builders__/:

  • aReservation() with state, items, channel, fxSnapshot helpers (e.g., .held(), .confirmed(), .checkedIn()).
  • aGuest() with locale, name script, document presets.
  • anEvent() envelope builder; aPubSubMessage() adapter.
  • aTenant() with policy snapshots and holdTtlSeconds.

Builders are pure-TS, framework-free, and produce types that satisfy the strict domain interfaces.


10. Cross-references