iam-service — Testing Strategy
iam-service holds the keys to the kingdom. Test rigour exceeds the platform baseline: ≥ 90 % line coverage on domain layer, ≥ 85 % on application layer, ≥ 80 % overall. Every security-impacting behavior has at least one negative test.
1. Test Pyramid
┌──────────────┐
│ Chaos / DR │ ← weekly
└──────────────┘
┌──────────────┐
│ Load / Perf │ ← per release + nightly
└──────────────┘
┌──────────────┐
│ Security │ ← per release + on every auth change
└──────────────┘
┌──────────────────┐
│ E2E │ ← per PR (smoke), nightly (full)
└──────────────────┘
┌──────────────────────┐
│ Pact / Contract │ ← per PR
└──────────────────────┘
┌──────────────────────────┐
│ Integration (Postgres, │ ← per PR
│ Redis, Pub/Sub, KMS) │
└──────────────────────────┘
┌──────────────────────────────────┐
│ Unit │ ← per PR
└──────────────────────────────────┘
2. Coverage Targets
| Layer | Line | Branch | Mutation |
|---|---|---|---|
domain/ | 95 % | 90 % | ≥ 70 % |
application/ | 90 % | 85 % | ≥ 60 % |
infrastructure/ | 80 % | 70 % | n/a |
presentation/ | 80 % | 70 % | n/a |
| Overall | ≥ 80 % | ≥ 75 % | n/a |
CI gate: failing → block merge. Coverage report uploaded to Codecov.
3. Tooling
| Layer | Stack |
|---|---|
| Unit | Vitest + @testing-library where DOM applies; pure functions elsewhere |
| Property-based | fast-check |
| Integration | Vitest + Testcontainers (Postgres, Redis, fake-pubsub, fake-kms) |
| Contract | Pact 4 (consumer + provider) |
| E2E | Playwright (against full local stack via docker compose -f docker-compose.e2e.yml) |
| Security | OWASP ZAP, Semgrep, Trivy/Snyk; custom scripts |
| Load | k6 |
| Chaos | Toxiproxy + custom failure injection |
| Mutation | Stryker (domain only) |
4. Unit Suites (key)
4.1 User aggregate
| Test | Asserts |
|---|---|
requestEmailVerification sets token + status | invariant |
Cannot login if status != active | invariant |
| Lock idempotent | repeated calls don't multiply events |
| Unlock requires actorId | guard |
| GDPR erasure anonymizes email + emits event | invariant |
4.2 Credential
| Test | Asserts |
|---|---|
| Verify with correct password → success, no rehash if version current | behavior |
Verify with stale hash_version → success + rehash event | rehash on login |
| Verify wrong password → failure, no leak | timing test (next section) |
change() rejects passwords in last 5 history entries | policy |
4.3 Session family
| Test | Asserts |
|---|---|
Refresh rotates currentTokenHash, appends to previousTokenHashes | invariant |
Re-presenting a previousTokenHashes member → entire family revoked | reuse detection |
| Bound session cannot refresh on different device | binding |
| Revoke is idempotent | invariant |
4.4 Device
| Test | Asserts |
|---|---|
| Bind requires fresh-auth ACR | precondition |
| Bind generates Ed25519 cert serial unique per device | invariant |
| Revoke cascades to sessions bound to device | side-effect |
Renewal preserves deviceId and binding history | invariant |
4.5 MFAFactor
| Test | Asserts |
|---|---|
| TOTP enroll requires verification before active | invariant |
| WebAuthn sign_count regression marks credential cloned | security |
| Recovery codes are single-use | invariant |
| Removing last factor when MFA mandatory → policy violation | guard |
4.6 APIKey
| Test | Asserts |
|---|---|
| Issuance returns plaintext only once | invariant |
| Validation against expired key fails | TTL |
| Validation against revoked key fails (via denylist after replication) | revoke |
| Scope check enforced | RBAC |
4.7 Domain Services
PasswordPolicyService: strength rules; breach-list k-anonymity request shape; history check.AdaptiveMFAService: AI raise-only invariant; fallback path; tenant override (require always).TokenSigner(port test against fake KMS): signs valid Ed25519, rejects on badkid.
5. Integration Suites (Testcontainers)
Spawns: Postgres 15, Redis 7, fake-pubsub, fake-kms, mock-idp (OIDC + SAML), mailhog.
| Suite | Coverage |
|---|---|
user.repo.spec.ts | CRUD + RLS via session vars |
credential.repo.spec.ts | History append + truncate to 5 |
session.repo.spec.ts | Rotation, reuse-detection, family revoke |
device.repo.spec.ts | Cert serial uniqueness, revoke cascade |
outbox.spec.ts | Atomic write with domain row in same tx; relay drains; DLQ on persistent failure |
inbox.spec.ts | Idempotent consume; out-of-order safe (latest-wins where stated) |
oidc.callback.spec.ts | Full flow against mock-idp; nonce/state validation; PKCE |
saml.assertion.spec.ts | Assertion signature; XSW negative; clock-skew tolerance |
magic-link.spec.ts | TTL, single-use, replay-blocked |
device.bind-offline.spec.ts | CSR → cert; tenant CA chain validation |
kms.signing.spec.ts | Sign + verify; rotation overlap; emergency rotation |
gdpr.erasure.spec.ts | Saga participation; idempotent; audit retained |
6. Contract Tests (Pact)
6.1 Consumer-side (this service consumes)
| Provider | Contract |
|---|---|
tenant-service | tenant.created.v1, tenant.deleted.v1, tenant.guest.erasure_requested.v1 event shapes |
notification-service | notification.dispatch (used to send magic-link / reset emails) |
ai-orchestrator-service | ai.classify.login_risk request/response |
6.2 Provider-side (this service provides)
| Consumer | Contract |
|---|---|
tenant-service | iam.user.registered.v1, iam.user.locked.v1, iam.user.erased.v1 |
audit-service | All audit events listed in SECURITY_MODEL §12 |
| every service | /.well-known/jwks.json shape; JWT claim shape |
gdpr-service | iam.user.erased.v1 |
Pact broker: https://pact.melmastoon.cloud. Provider verification job runs on every iam PR.
7. End-to-End (Playwright)
E2E stack: full docker compose (api gateway + iam + tenant + notification + mock-idp + mailhog + postgres + redis + fake-pubsub).
7.1 Smoke (every PR; ≤ 5 min)
- Register → verify email → login → fetch profile → logout
- Login + MFA TOTP enroll + challenge
- Refresh token round-trip
- Magic link request → consume
7.2 Full (nightly; ≤ 30 min)
- SSO OIDC end-to-end with mock-idp
- SSO SAML end-to-end with mock-idp
- WebAuthn enroll + login (using
@simplewebauthnvirtual authenticator) - Device register → trust → bind-for-offline → renew
- Offline mode: cut network, refresh succeeds with offline cert; reconnect; sync resumes
- Password reset request → email link → reset → old refresh tokens invalidated
- Account lock by admin → user logged out → unlock by admin
- API key issue → use → rotate → revoke
- GDPR erasure: tenant emits guest erasure → user disappears within SLA
- Tenant deleted → all sessions revoked
8. Security Tests
8.1 Automated per release
| Test | Expectation |
|---|---|
| OWASP ZAP baseline | no high findings |
| Semgrep (security ruleset) | clean |
| Trivy / Snyk SCA | no critical CVEs |
| Secret scan (gitleaks) | clean |
| Dependency confusion check | clean |
npm audit --omit=dev | clean (high/critical) |
8.2 Custom security scenarios
| Scenario | Mechanism |
|---|---|
| Credential stuffing | Replay 10 000 known-bad creds; assert lockout + WAF rate limit |
| Refresh-token theft | Steal refresh, attacker uses, then victim presents → both blocked + family revoked + alert |
| SSO callback tampering | Modify state, nonce, signature → all rejected |
| SAML XSW | Inject wrapped assertion → rejected |
| MFA bypass attempt | Submit mfa_token for different user → rejected |
| Magic-link replay | Consume twice → second rejected |
| Account enumeration via timing | Statistical test: register/login/reset response time difference for known vs unknown email is < 5 ms |
| API key reuse after revoke | Use within Redis denylist TTL → blocked |
| JIT SSO substitution | IdP returns claim for different tenant → rejected |
| Device fingerprint spoof | Submit hand-crafted fingerprint → bind requires fresh-auth + Ed25519 → spoofed bind cannot login another session |
| JWKS poisoning | Publish unauthorized key → consumer rejects (signature verify) |
| JWT replay after revoke | Token in iam:revoked:jti:* → API gateway rejects |
| Rate limit bypass attempts | Header injection / IP spoofing → blocked at WAF |
8.3 Cryptographic tests
- Argon2id parameter compliance (m, t, p match config)
- Ed25519 sign / verify round-trip with KMS
- HKDF-derived bundle key matches client implementation (cross-language KAT)
- TOTP RFC 6238 compliance KAT vectors
9. Load Tests (k6)
| Scenario | Target |
|---|---|
| Steady login | 500 RPS sustained for 30 min, p95 < 200 ms, p99 < 800 ms |
| Refresh burst | 2 000 RPS for 10 min, p95 < 100 ms |
| JWKS read | 10 000 RPS for 10 min, p95 < 20 ms (CDN-backed) |
| Cold start | first 60 s after deploy: error rate < 0.5 % |
| MFA challenge | 200 RPS, p95 < 200 ms |
| Worst-case attack | 5 000 RPS of failed logins → graceful degradation, lockouts triggered, no 5xx |
Load profile recorded as baseline; regression > 20 % blocks release.
10. Chaos Engineering
Run weekly in staging via Toxiproxy.
| Scenario | Expected |
|---|---|
| KMS adds 2-s latency | Login p99 elevated but < 2 500 ms; no cascading failure |
| Postgres primary failover | < 30 s unavailability; auto-recover |
| Redis cluster partition | Login still works (DB fallback for rate-limit); MFA throttles fallback to in-memory |
| Pub/Sub publish errors 50 % | Outbox retries; no data loss |
| Notification-service down | Magic-link / reset return 202; queued; eventually delivered |
| Mock-IdP timeout | OIDC callback returns 504 problem; user can fall back to password |
| AI orchestrator down | Adaptive MFA falls back to rules; counter iam_ai_fallback_total increments |
11. Negative-Path Coverage Catalog
Maintained in test/negative-paths.csv; CI asserts every row has ≥ 1 covering test.
| Code | Path |
|---|---|
MELMASTOON.IAM.AUTH.INVALID_CREDENTIALS | login wrong password |
…AUTH.MFA_REQUIRED | login when MFA enrolled |
…AUTH.MFA_INVALID | wrong TOTP |
…AUTH.ACCOUNT_LOCKED | locked user login |
…AUTH.RATE_LIMITED | exceed throttle |
…AUTH.PASSWORD_BREACHED | reset with breached password |
…AUTH.MAGIC_LINK_EXPIRED | TTL exceeded |
…AUTH.MAGIC_LINK_USED | replay |
…SSO.STATE_MISMATCH | tampered state |
…SSO.NONCE_REPLAY | reused nonce |
…SSO.ASSERTION_INVALID | SAML signature broken |
…DEVICE.BIND_REQUIRES_FRESH_AUTH | stale ACR |
…DEVICE.OFFLINE_LIMIT_EXCEEDED | last-seen > 7 d |
…SESSION.ROTATION_REUSE | token theft |
…APIKEY.SCOPE_INVALID | wrong scope |
…APIKEY.REVOKED | post-revoke use |
12. Test Data
- Faker-generated tenants, users, devices in
test/fixtures/. - Deterministic seed (
MELMASTOON_TEST_SEED=42) → reproducible runs. - No production data ever copied (synthetic-only policy).
- Snapshot tests for OpenAPI + AsyncAPI; drift fails CI.
13. Definition of Done — Test Aspect
Per SERVICE_READINESS:
- All new/changed paths covered by unit + integration tests.
- Negative-path catalog entry exists for every new error code.
- Pact contract update merged in
pact-brokerif shape changes. - E2E smoke updated if user-facing flow changes.
- Coverage thresholds green.
- ZAP baseline + Semgrep clean.
- Load baseline within 20 % of previous release.