Skip to main content

property-service — TESTING_STRATEGY

Companion: DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS · SYNC_CONTRACT · SECURITY_MODEL · FAILURE_MODES

This document is the binding test specification for property-service. It defines the test pyramid layout, coverage gates, scenario lists per layer, contract/consumer-driven testing posture, low-bandwidth and offline chaos, security tests, and CI gating.

Frameworks: Vitest for unit + integration; Pact (provider verification) for HTTP contracts; AsyncAPI + Avro schema diff for events; Testcontainers for Postgres + Pub/Sub emulator + Memorystore; Playwright at the BFF/UX layer (out of this service's direct ownership but referenced).


1. Test Pyramid

LayerFrameworksCoverage gateWhereOwns
Unit (domain + use cases)Vitest, fast-check90 % lines, ≥ 85 % branches in src/domain/** and src/application/****/*.spec.tsAggregate invariants, value object validation, command handlers with mocked ports
IntegrationVitest + Testcontainers (Postgres 15 + PostGIS, Pub/Sub emulator, Memorystore emulator)80 % lines in src/infrastructure/****/*.int-spec.tsRepository SQL, RLS enforcement, outbox/inbox transactional semantics, cache key behavior
Contract — HTTP (provider)Pact verification against contracts published by bff-backoffice-service, bff-tenant-booking-service, search-aggregation-service, housekeeping-service100 % of consumer states passpacts/**/*.jsonAPI stability
Contract — Events (producer)AsyncAPI lint + schema-registry compatibility check0 breaking changes without an Xxx.v(n+1) topicevents/asyncapi.yamlEvent stability
End-to-end (service-local)Vitest with full stack via docker composesmoke + critical pathse2e/**/*.e2e-spec.tsRealistic create→publish→archive flows
Cross-service E2EPlaywright at the BFF + UI levelplatform-ownedexternalBooking happy path; smoke ownership not in this service

Coverage gates are enforced in CI; below threshold = failing build.


2. Unit Tests

2.1 Domain (≥90 % required)

Aggregate-level scenarios — every invariant from DOMAIN_MODEL §3:

  • Property.publish rejects when rooms.count(active) < 1MELMASTOON.PROPERTY.NO_ACTIVE_ROOMS_TO_PUBLISH.
  • Property.publish rejects when default-locale translation missing.
  • Property.publish rejects when geo missing.
  • Property.publish rejects when no hero photo.
  • Property.archive rejects when status is published (must unpublish first).
  • Room.changeStatus enforces transition matrix; rejects illegal transitions with MELMASTOON.PROPERTY.ILLEGAL_STATUS_TRANSITION.
  • Room.changeStatus(active → out_of_order) requires non-empty reason.
  • Room.archive rejects when there is an active reservation reference (provided via port; mocked).
  • RoomType code uniqueness per property (handled at repo, but assertion lives at use case).
  • BedConfig: exactly one default per room type.
  • Photo.setHero enforces single hero per property invariant.
  • PolicyOverride.set validates kind and shape.
  • PropertyAmenity.set deduplicates and rejects unknown codes.
  • Property.translations.upsert requires at least one locale to be the configured defaultLocale.
  • Property/Room cross-tenant constructor rejects with CrossTenantReferenceError.

2.2 Property-based (fast-check)

  • For any non-empty room set with status drawn from the lifecycle, applying any valid transition sequence preserves the invariant archived ⇒ no further transitions.
  • For any sequence of setHero calls, only one row ends with is_hero=true.
  • For any address with mixed Latin + native script, the canonical translation upsert preserves both fields losslessly.
  • For any geohash bounding box and N synthetic properties uniformly distributed, the in-memory geo filter agrees with the PostGIS-backed one (sample N=1000).

2.3 Use cases

Every command handler (commands/*.handler.ts) has at least:

  • happy path
  • precondition rejected (returns specific error code)
  • authorization denied path (mocked AuthorizationPort)
  • tenant mismatch path
  • idempotency replay path (same key, same body → no-op; same key, different body → MELMASTOON.GENERAL.IDEMPOTENCY_KEY_REPLAYED_DIFFERENT_BODY)

Every query handler has at least:

  • happy path
  • empty result
  • RLS-driven empty result (caller from another tenant)

3. Integration Tests

3.1 Persistence

  • Every multi-tenant table must have a *_tenant_isolation.int-spec.ts that:
    1. Inserts row R as tenant A.
    2. Switches connection context to tenant B (SET LOCAL app.tenant_id).
    3. Asserts: SELECT … WHERE id = R.id returns 0 rows.
    4. Asserts: UPDATE and DELETE of R from tenant B context affect 0 rows.
  • Repository SQL: every method exercised against real PostGIS-enabled Postgres.
  • Geo:
    • bounding box query returns expected fixture set.
    • nearby (radius) query orders by distance.
    • GIST index used (verified via EXPLAIN).
  • Outbox:
    • aggregate save + outbox row are written in the same transaction; rollback removes both.
    • publisher marks published_at, increments attempts on failure.
    • DLQ path on max attempts.
  • Inbox:
    • duplicate messageId is short-circuited.
    • failure path keeps processed_at null and increments attempts.
  • Cache:
    • hit / miss / fill telemetry asserted.
    • invalidation on property.updated.v1 clears expected keys.

3.2 Event consumers

  • tenant.created.v1 → tenant becomes "allowed" for property creation.
  • tenant.deleted.v1 → cascade unpublish + archive sequence emitted.
  • housekeeping.room.maintenance_required.v1 → auto-OOO; ignored when room is already OOO/OOS/archived.
  • maintenance.work_order.completed.v1 → auto-RTS only when previous OOO source was a maintenance event with the same work-order id; otherwise no-op.
  • Consumer is idempotent (replays produce the same final state).

3.3 Sync

  • Pull endpoint paginates correctly across cursor boundaries.
  • If-None-Match returns 304-style empty payload when nothing changed.
  • Push: each conflict resolution path covered (fast_forward, lww_status, notes_three_way_merge, hero_lww, tombstone).
  • Push: rejection paths exercised (expectedVersion mismatch, illegal transition).

4. Contract Tests

4.1 HTTP provider verification (Pact)

Verify against pacts published by:

  • bff-backoffice-service (operator console)
  • bff-tenant-booking-service (tenant booking site catalog)
  • search-aggregation-service (consumer meta projection refresh)
  • housekeeping-service (room status feedback) — internal RPC, also via Pact
  • lock-integration-service (PUT /rooms/:id/lock-binding)

Provider states required:

  • a tenant exists with id tnt_seed
  • a published property exists with id ppt_seed
  • a draft property exists with no rooms
  • a property exists with 5 active rooms
  • a room exists with status active
  • a room exists with status out_of_order
  • an idempotency key has been used

Failure to honor any contract = failing build; consumers cannot land breaking changes without coordinated pact update.

4.2 Event producer compatibility

  • Every published topic in EVENT_SCHEMAS has an Avro/JSON-schema in events/schemas/. The CI gate runs schema-diff against the registry; only forward-compatible changes pass on the same vN. Breaking changes require a new vN+1 topic and an explicit deprecation note.

5. End-to-End (service-local)

e2e/** scenarios run against the full docker compose with wiremock for iam-service, file-storage-service, ai-orchestrator-service, and geo-service:

  1. Create → publish → unpublish → archive of a property in Kabul, including translations (ps-AF, en, fa), hero photo, 3 room types, 12 rooms.
  2. Bulk room create (50 rooms) — all-or-nothing atomicity verified.
  3. OOO from front desk — push from sync, server emits expected event, downstream wiremock receives it.
  4. Photo upload happy path — signed URL, upload, scan-clean event, photo becomes ready, event emitted.
  5. Photo quarantine — scan event with verdict infected; photo not promoted; operator notified path captured.
  6. AI describe-draft accept — staged suggestion → operator accepts → translation row updated with aiProvenance.
  7. Tenant deletion cascadetenant.deleted.v1 consumed; properties archived, photos removed, events emitted in order.
  8. Reservation-block on room archive — reservation port mock returns "active reservation"; archive rejected with the expected code.
  9. Cross-tenant isolation — operator A cannot read or mutate operator B's property by direct ID.

6. Low-Bandwidth & Offline Chaos

  • Sync 24h offline replay. Generate 200 status changes offline; verify push completes within 60 s and produces correct event stream (in monotonically increasing order per room).
  • High latency (RTT 1.5 s). Pull/push still completes within SLO with adjusted thresholds; backpressure observed.
  • Packet loss (10 %). No duplicates after replay (idempotency).
  • Partial cursor regression. Force a server cursor regression scenario; client recovers via the documented full-reset fallback (SYNC_CONTRACT §7).
  • Pub/Sub outage. Outbox grows; publisher backs off; on recovery, drains within 5 min for a 10k-row backlog.
  • Cloud SQL failover. Reads succeed on read replica; writes fail-fast with MELMASTOON.GENERAL.SERVICE_DEGRADED until primary returns; no data loss.

7. Security Tests

  • AuthN. Reject expired tokens, wrong aud, wrong iss, missing kid, JWKS rotation honored.
  • AuthZ. Per-route allow/deny matrix exercised; OPA policy decisions are unit-tested separately and verified against the actual bundle in CI.
  • Tenant isolation. Per §3.1; plus a fuzzing job that crafts random property IDs as tenant B and confirms 404 (not 403) leakage of existence.
  • Idempotency replay. Verified per use case.
  • Signed URL safety. Photo uploads cannot exceed declared size; mime-mismatch payload is rejected by file-storage-service and the row stays uploaded.
  • SQLi & path traversal smoke. Static (semgrep) + dynamic (zaproxy in CI) — gating only on high severity.

8. Performance & Load

  • Read baseline. k6 script: 200 RPS for GET /properties/:id on cached path; p99 < 80 ms; cache hit > 95 %.
  • Cold geo-search. GET /properties/geo/nearby 50 RPS, p99 < 400 ms with PostGIS GIST index, no cache.
  • Bulk room create. 100 rooms in one request completes < 2 s and emits 100 distinct property.room.created.v1 events.
  • Photo pipeline. 50 concurrent uploads complete uploaded → ready within p95 < 30 s.
  • Sync push. 60 push requests/min/device sustained; no 5xx.

9. Test Data Strategy

  • Deterministic ULIDs in tests via a seeded factory; never wall-clock now() directly in assertions.
  • fixtures/ ships seeds for the 3-property local-dev scenario (Kabul, Dushanbe, Tehran).
  • Property names and addresses include mixed RTL/LTR scripts in fixtures to surface bidi rendering bugs early.

10. CI Gating

Every PR must pass:

  1. lint (eslint --max-warnings 0)
  2. type check (tsc --noEmit)
  3. unit tests + coverage gates
  4. integration tests (Testcontainers)
  5. provider Pact verification (against the latest published consumer pacts)
  6. AsyncAPI lint + schema compatibility check
  7. semgrep + npm audit (high-severity = block)
  8. OpenAPI lint + breaking-change diff (block on undeclared breaking change)

Nightly:

  • E2E suite (full docker compose)
  • Performance baseline (drift > 20 % vs last green = ticket)
  • Tenant isolation fuzzer (1 hour)

Runbooks for failure scenarios live alongside FAILURE_MODES; test seed data is documented in LOCAL_DEV_SETUP.