Skip to main content

TESTING_STRATEGY — lock-integration-service

Bundle: SERVICE_OVERVIEW · DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · LOCAL_DEV_SETUP · FAILURE_MODES

Cross-cutting: docs/02 §12 Quality, SERVICE_TEMPLATE — TESTING_STRATEGY.

This service can never be tested only against vendor sandboxes — vendor sandboxes have low rate limits, slow latency, and inconsistent behavior. Therefore the strategy is layered: fast in-memory tests against contract-conformant simulators, plus periodic verification against real vendor sandboxes.

1. Pyramid

LayerCount targetToolingRuntime
Domain unit tests250+vitest + plain TS, no I/O< 8s total
Application unit tests (use cases, sagas)120+vitest with in-memory ports + MockClock< 25s
Adapter contract tests60+ per vendorvitest with the vendor simulator< 60s per vendor
Integration tests (Postgres + Pub/Sub emulator + outbox + adapters with simulators)80+vitest + Testcontainers< 4 min
Webhook ingestion tests (HTTP layer + signature verification + inbox dedup)30+supertest + signed body fixtures< 30s
End-to-end sync round-trip (cloud + fake desktop)12+vitest integration; in-process electron-main shim< 90s
Vendor sandbox conformance suite (real network)25 per vendorvitest with LIVE_VENDOR=truenightly only
Performance/load (saga throughput, outbox publish)6 scenariosk6 against stagingweekly

2. Domain unit tests

Cover all aggregates from DOMAIN_MODEL:

  • KeyCredential state-machine transitions: every legal transition + every illegal one returns MELMASTOON.LOCK.STATE_INVARIANT_VIOLATED.
  • KeyCredential.update: validates validFrom < validUntil, room-set is non-empty, scope additions don't exceed policy.
  • MasterKey: scope must be ≤ staff role's permitted areas; expiry must align with shift end + grace.
  • KeyKindPolicy: preferred-order enumerates only kinds permitted for the property's vendor mix.
  • OfflineIssuance: validUntil ≤ validFrom + 14d, maxIssuances ≤ 200.

All tests pure — no Postgres, no Pub/Sub, no clock dependency (inject Clock port).

3. Vendor simulators

The cornerstone of fast adapter testing. Each vendor has a deterministic in-process simulator implementing the same LockAdapter interface as the real adapter, plus a simulator API for orchestrating scenarios. They live in packages/lock-vendor-simulators/.

SimulatorCapabilities
TtLockSimulatorMints opaque vendor refs; emits webhook callbacks via in-memory channel; configurable latencies and failure injection (forceTimeout, forceCode('VENDOR_QUOTA')); BLE pairing flow stubbed
SaltoSimulatorXS4 cloud + on-prem connector dual-mode; supports "connector offline" mode that returns 503
VostioSimulatorCloud-only; supports webhook ack with delay; supports cred rotation cycle
GenericWiegandSimulatorEncoder USB session simulator; supports open, writeCard, readCard, close; failure injection: cardJam, disconnect, wrongFormat

Adapter contract tests run the same suite against the real adapter (against sandbox, opt-in) and the simulator. The suite asserts:

  1. Idempotency: same idempotencyKey returns the same vendorRef.
  2. Capability matrix matches LockAdapter.capabilities() declaration.
  3. revoke is idempotent.
  4. update rejects out-of-bounds windows with the documented error code.
  5. Webhook payloads match WebhookHandler parser expectations.

This is how we know the simulator is a faithful substitute.

4. Application unit tests (sagas)

Use cases and sagas tested with in-memory ports:

  • IssueSagaTest covers all branches in APPLICATION_LOGIC §4: happy path, vendor timeout (retry), vendor permanent failure (compensation), advisory-lock contention, capability fallback (mobile_app → pin_code).
  • RevokeSagaTest: happy path, vendor 404 treated as success, compensation when vendor reports partial revocation.
  • MasterKeyShiftSagaTest: includes off-shift early start, scope reduction at handover.
  • WebhookSagaTest: dedup, invalid signature, body-tenant mismatch.
  • OfflineReconcileSagaTest: every branch of the decision matrix in SYNC_CONTRACT §5.1.

Each saga test uses a MockClock so retries with backoff are deterministic. Each test asserts both the final aggregate state and the exact set of outbox events emitted.

5. Integration tests

Spin up via Testcontainers:

  • Postgres 15 with init schema + RLS policies applied.
  • Google Pub/Sub Emulator.
  • Wire real outbox publisher and inbox consumer.
  • Use simulators for vendor adapters.

Scenarios:

  • Reservation confirmed → outbox event → emulator → inbox handler → IssueSaga → vendor simulator → lock.credential.issued.v1 published.
  • Same event replayed → inbox dedup → no duplicate work, no duplicate outbox row.
  • Concurrent issue for same room → advisory lock serializes; one succeeds, one waits then succeeds with idempotent ack.
  • DB failure mid-saga (kill Postgres mid-test) → recovery on restart picks up via outbox + inbox.
  • RLS verification: a tenantA-scoped session cannot read tenantB rows.

6. Webhook ingestion tests

  • Signed-body fixtures per vendor (committed under tests/fixtures/webhooks/{vendor}/).
  • Signature mismatch → 401, no inbox row.
  • Replay (same vendorEventId) → 200, no duplicate processing.
  • Cross-tenant attempt (vendorAdapterId in path doesn't belong to tenant in body) → 403.
  • Body too large (> 1 MiB) → 413.
  • mTLS test for Vostio uses test client cert.

7. End-to-end sync round-trip

A fake Electron desktop is stood up as an in-process electron-main shim:

  • Pulls active credentials horizon.
  • Issues a provisional credential locally; pushes via /sync/v1/push.
  • Cloud reconciler validates, materializes; emits canonical event.
  • Fake desktop pulls again; sees non-provisional row.

Failure scenarios:

  • Push with revoked offline cert → outcome rejected, desktop revokes locally.
  • Push for a cancelled reservation → outcome revoked.
  • Cursor out of range → 410, desktop performs full rebase.

8. Vendor sandbox conformance suite

Runs nightly in CI against real vendor sandboxes (TTLock dev, Salto sandbox, Vostio test, plus a physical Generic Wiegand encoder mounted in a hardware lab with serialport). Subset of the adapter contract suite that doesn't burn quota:

  • Issue + revoke a credential.
  • Update validity window.
  • Verify webhook delivery within 60s.
  • Verify capability declaration matches what the sandbox accepts.

Failures here generate Jira tickets, not page-the-on-call (vendor sandboxes are flaky by nature). Three consecutive nightly failures escalate to vendor-relationship owner.

9. Hardware-in-the-loop (Generic Wiegand)

  • Lab rig with a USB Wiegand encoder + 2 RFID cards + 1 demo lock.
  • Test runner uses node-hid and serialport directly to validate that the simulator's behavior matches reality.
  • Runs weekly; manual checklist for visual confirmation that the lock opens with the issued card.

10. Coverage targets

ComponentLineBranchMutation (Stryker)
Domain95%90%80%
Application (use cases, sagas)92%88%75%
Adapters85%80%60%
Presentation (HTTP)85%80%n/a
Overall90%85%70%

Mutation testing (Stryker) runs weekly on the domain + application layers; surviving mutants on the saga state machine block release.

11. Performance / load

k6 scenarios in tests/k6/:

  • 50 issue sagas/sec sustained for 10 min; assert p95 < 5s.
  • 200 webhook callbacks/sec sustained; assert inbox lag p95 < 5s.
  • 1000 active credentials/property × 100 properties/tenant × 10 tenants — verify horizon-pull payload < 1 MiB and < 2s server time.
  • Outbox publisher backpressure test: publisher artificially slowed; assert backlog drains within 5 min after recovery.

Run weekly against staging; baseline saved per release.

12. Test data

  • Synthetic data: a deterministic seed produces tenant + property + room + reservation graph used across integration and load tests.
  • No real vendor data in the repo. Vendor sandbox credentials live only in CI secret store.
  • PII rules: synthetic guests use generated names tagged __synthetic__ so any leak detection can flag them.

13. Pre-merge gates

GateRequired
pnpm lint cleanYes
pnpm typecheck cleanYes
Unit + application + adapter (simulator) + integration tests passingYes
Coverage thresholds metYes
Mutation score (domain + application) ≥ targetWeekly job; PR-warn only
Webhook fixture signatures verifiedYes
OpenAPI spec diff reviewed if presentation/ touchedYes

14. Pre-release gates

GateRequired
Vendor sandbox conformance green for last 3 nightly runsYes
k6 weekly load run within 10% of baselineYes
HIL Generic Wiegand checklist run within last 14 daysYes
Manual smoke: pair an Electron desktop, issue + revoke a credentialYes

15. Cross-references