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
| Layer | Count target | Tooling | Runtime |
|---|---|---|---|
| Domain unit tests | 250+ | vitest + plain TS, no I/O | < 8s total |
| Application unit tests (use cases, sagas) | 120+ | vitest with in-memory ports + MockClock | < 25s |
| Adapter contract tests | 60+ per vendor | vitest 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 vendor | vitest with LIVE_VENDOR=true | nightly only |
| Performance/load (saga throughput, outbox publish) | 6 scenarios | k6 against staging | weekly |
2. Domain unit tests
Cover all aggregates from DOMAIN_MODEL:
KeyCredentialstate-machine transitions: every legal transition + every illegal one returnsMELMASTOON.LOCK.STATE_INVARIANT_VIOLATED.KeyCredential.update: validatesvalidFrom < 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/.
| Simulator | Capabilities |
|---|---|
TtLockSimulator | Mints opaque vendor refs; emits webhook callbacks via in-memory channel; configurable latencies and failure injection (forceTimeout, forceCode('VENDOR_QUOTA')); BLE pairing flow stubbed |
SaltoSimulator | XS4 cloud + on-prem connector dual-mode; supports "connector offline" mode that returns 503 |
VostioSimulator | Cloud-only; supports webhook ack with delay; supports cred rotation cycle |
GenericWiegandSimulator | Encoder 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:
- Idempotency: same
idempotencyKeyreturns the samevendorRef. - Capability matrix matches
LockAdapter.capabilities()declaration. revokeis idempotent.updaterejects out-of-bounds windows with the documented error code.- Webhook payloads match
WebhookHandlerparser 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:
IssueSagaTestcovers 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.v1published. - 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-hidandserialportdirectly 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
| Component | Line | Branch | Mutation (Stryker) |
|---|---|---|---|
| Domain | 95% | 90% | 80% |
| Application (use cases, sagas) | 92% | 88% | 75% |
| Adapters | 85% | 80% | 60% |
| Presentation (HTTP) | 85% | 80% | n/a |
| Overall | 90% | 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
| Gate | Required |
|---|---|
pnpm lint clean | Yes |
pnpm typecheck clean | Yes |
| Unit + application + adapter (simulator) + integration tests passing | Yes |
| Coverage thresholds met | Yes |
| Mutation score (domain + application) ≥ target | Weekly job; PR-warn only |
| Webhook fixture signatures verified | Yes |
OpenAPI spec diff reviewed if presentation/ touched | Yes |
14. Pre-release gates
| Gate | Required |
|---|---|
| Vendor sandbox conformance green for last 3 nightly runs | Yes |
k6 weekly load run within 10% of baseline | Yes |
| HIL Generic Wiegand checklist run within last 14 days | Yes |
| Manual smoke: pair an Electron desktop, issue + revoke a credential | Yes |
15. Cross-references
- LOCAL_DEV_SETUP — running the simulators
- SERVICE_READINESS — gate map
- 02 §12 Quality