Skip to main content

numbering-service — Testing Strategy

Version: 1.0 Status: Draft Owner: Commerce Engineering + Platform QE Last Updated: 2026-04-21 Companion: APPLICATION_LOGIC · SECURITY_MODEL · FAILURE_MODES


1. Test Pyramid

┌──────────────────────┐
│ E2E (5–8 flows) │ Playwright + k6 smoke
├──────────────────────┤
│ Contract (Pact × 6) │ Orchestrator / router / sender-id / portal
├──────────────────────┤
│ Integration (60+) │ Testcontainers PG + Redis + NATS
├──────────────────────┤
│ Unit (250+) │ State machine, VOs, validators, quotas
└──────────────────────┘

Coverage target: ≥ 85 % line, ≥ 80 % branch per common/testing.md (with 80 % min enforced in CI).


2. Unit Tests

Framework: Jest + ts-jest.

2.1 Value objects & validators

describe('Msisdn VO', () => {
it('accepts valid Afghan E.164', () => {
expect(Msisdn.parse('+93701234567').isOk).toBe(true);
});
it('rejects E.164 from other countries when country-locked mode is on', () => {
expect(Msisdn.parseStrictAf('+14155551212').isOk).toBe(false);
});
it('rejects too-short numbers', () => {
expect(Msisdn.parse('+9370').isOk).toBe(false);
});
it('rejects malformed with letters', () => { ... });
});

describe('ShortCode VO', () => {
it('accepts 4-digit, 5-digit, 6-digit', () => { ... });
it('rejects 3-digit and 7-digit', () => { ... });
it('rejects non-numeric', () => { ... });
});

describe('AlphaId VO', () => {
it('accepts 1-11 GSM-7 default characters', () => { ... });
it('rejects lengths outside 1..11', () => { ... });
it('rejects extended-GSM characters that are not in default alphabet', () => { ... });
it('normalises for uniqueness (uppercase)', () => { ... });
});

2.2 State machine

describe('NumberState transitions', () => {
it('AVAILABLE → RESERVED is allowed', () => { expectValid('AVAILABLE','RESERVED'); });
it('AVAILABLE → LEASED requires bypass=true', () => { expectValidWithFlag('AVAILABLE','LEASED',{bypass:true}); });
it('RESERVED → HELD is allowed (same tenant)', () => { ... });
it('LEASED → AVAILABLE is NOT allowed (must recall)', () => { expectInvalid('LEASED','AVAILABLE'); });
it('QUARANTINE → RESERVED is NOT allowed', () => { expectInvalid('QUARANTINE','RESERVED'); });
it('RECALLED → QUARANTINE is only allowed for MSISDN/SHORT_CODE', () => { ... });
it('RECALLED → AVAILABLE is allowed for ALPHA_ID (no cool-off)', () => { ... });
});

2.3 Reservation / TTL

describe('Reservation TTL', () => {
it('RESERVE expires exactly 15 min after creation', () => { ... });
it('HOLD expires exactly 24 h after creation', () => { ... });
it('clock-skew tolerance ±5 s accepted', () => { ... });
});

2.4 Conflict & quota

describe('Conflict detection', () => {
it('identifies CAS race as CONFLICT', () => { ... });
it('detects prefix overlap in contract import', () => { ... });
});

describe('TenantQuotaEnforcer', () => {
it('rejects 51st MSISDN when cap=50', () => { ... });
it('counts RESERVED + HELD + LEASED against MSISDN quota', () => { ... });
it('does NOT count QUARANTINE against tenant quota (they no longer own)', () => { ... });
it('returns current and quota in the error payload', () => { ... });
});

2.5 Quarantine cool-off

describe('Quarantine cool-off', () => {
it('MSISDN default = 90 days', () => { ... });
it('Standard short code = 30 days', () => { ... });
it('Vanity short code = 365 days', () => { ... });
it('Alpha-ID = 0 days (direct return to AVAILABLE)', () => { ... });
it('admin override requires justification ≥ 20 chars', () => { ... });
});

2.6 MNO lease import validator

describe('LeaseImportValidator', () => {
it('rejects row with non-E.164 MSISDN', () => { ... });
it('rejects row whose prefix does not match operator', () => { ... });
it('rejects row where validUntil ≤ validFrom', () => { ... });
it('accepts duplicate and reports in summary (does not error)', () => { ... });
it('rejects CSV with invalid signature → 422 SIGNATURE_INVALID', () => { ... });
it('streams 100k rows without OOM', () => { ... }); // big-file test
it('rejects CSV with CRLF injection attempt in value field', () => { ... });
});

2.7 Homoglyph detection

describe('AlphaHomoglyph', () => {
it('flags "M0BI-BANK" as similar to "MOBI-BANK" (edit-distance 1)', () => { ... });
it('flags "Bank-xyz" as similar to "BANK-XYZ" (case-normalised)', () => { ... });
it('does NOT flag brand-distinct alphas', () => { ... });
it('does NOT flag within the same tenant', () => { ... });
});

2.8 Hash-chain audit

describe('Audit hash chain', () => {
it('computes sha256(prevHash || canonical_row) deterministically', () => { ... });
it('detects tamper when a row is modified', () => { ... });
it('detects missing rows in the chain', () => { ... });
});

3. Integration Tests

Framework: Jest + testcontainers (Postgres 15, Redis 7, NATS 2.10).

3.1 gRPC handler integration

describe('NumberingService gRPC integration', () => {
it('ValidateLease returns valid:true for LEASED identifier owned by caller', async () => { ... });
it('ValidateLease returns valid:false WRONG_TENANT cross-tenant', async () => { ... });
it('Reserve + Assign full flow', async () => { ... });
it('Reserve race: 100 concurrent goroutines → exactly one success, 99 CONFLICT', async () => { ... });
it('Assign rejects when alpha-ID unverified in sender-id-registry', async () => {
// mock sender-id-registry returning IsVerified=false
});
it('Recall with reason=REGULATOR_ORDER without ticketId → 422', async () => { ... });
it('Recall cascades quarantine with correct cool-off per type', async () => { ... });
it('ValidateLease returns UNAVAILABLE when PG + Redis both down', async () => { ... });
});

3.2 Reservation cleanup

describe('Reservation cleanup worker', () => {
it('Redis keyspace notification triggers release within 2 s', async () => { ... });
it('safety-net cron catches expired reservations missed by Redis', async () => { ... });
it('is idempotent across concurrent worker instances', async () => { ... });
it('emits number.released.v1 with reason=TTL_EXPIRED', async () => { ... });
});

3.3 Quarantine sweep

describe('Quarantine sweep', () => {
it('returns MSISDN to AVAILABLE after 90 d', async () => { ... });
it('admin override transitions immediately with justification', async () => { ... });
it('rejects admin override with justification < 20 chars', async () => { ... });
});

3.4 MNO lease batch import

describe('LeaseImport integration', () => {
it('imports 50k valid rows, summary matches', async () => { ... });
it('partial-invalid batch: inserts valid, writes invalid rows to error table', async () => { ... });
it('duplicate MSISDN across two batches is reported, not re-inserted', async () => { ... });
it('invalid signature rejected; zero rows inserted', async () => { ... });
});

3.5 RLS cross-tenant

describe('Row-Level Security', () => {
it('tenant A cannot SELECT tenant B leases', async () => { ... });
it('tenant portal GET /leases returns only own rows', async () => { ... });
it('admin role bypasses RLS with audit entry capturing cross-tenant access', async () => { ... });
});

3.6 Multi-region CAS

describe('Multi-region CAS', () => {
it('concurrent Reserve in kbl and mzr on same AVAILABLE number: exactly one wins', async () => { ... });
it('replication lag > 5 s raises cross-region-lag alert', async () => { ... });
});

3.7 Compliance & billing consumer flows

describe('Event consumers', () => {
it('compliance.tenant.suspended triggers bulk recall of all tenant leases', async () => { ... });
it('billing.account.delinquent transitions leases to SUSPENDED, then RECALLED after grace', async () => { ... });
it('billing.account.paid reinstates SUSPENDED leases within grace', async () => { ... });
it('tenant.deleted releases reservations and recalls all leases', async () => { ... });
});

4. Contract Tests (Pact)

Verified on every CI run:

ConsumerProviderContract
sms-orchestratornumbering-serviceValidateLease gRPC — request/response shape, reason codes
routing-enginenumbering-serviceLookup gRPC — metadata shape
sender-id-registry-servicenumbering-serviceLookup, Recall
customer-portal-bffnumbering-serviceREST Reserve/Assign/Release
billing-servicenumbering-serviceNATS event schema number.assigned.v1, .released.v1, .renewed.v1, .recalled.v1
regulator-portal-servicenumbering-serviceNATS numbering.regulator.export.generated.v1

Pact broker at pact.ghasi.internal. Consumer-driven: consumer publishes expected interactions, provider verifies on each PR.


5. Load Tests

Framework: k6 (REST) + ghz (gRPC). Runs nightly + pre-release.

5.1 Hot-path ValidateLease

ghz --proto src/proto/numbering.proto \
--call ghasi.sms.numbering.v1.NumberingService.ValidateLease \
--data-file ./test/load/valid_request.json \
--cert ./test/load/client.crt --key ./test/load/client.key --cacert ./test/load/ca.crt \
--concurrency 200 --total 500000 \
--host localhost:50061

Pass criteria:

  • 5000 RPS sustained for 10 minutes
  • P50 ≤ 8 ms · P95 ≤ 20 ms · P99 ≤ 50 ms (cache-hit mix ≥ 95 %)
  • Error rate < 0.05 %
  • No memory leak (RSS stable within ±5 % over run)

5.2 Lifecycle mix

5 % Reserve · 3 % Assign · 2 % Release · 90 % ValidateLease under 2000 RPS for 30 minutes.

Pass criteria:

  • Reserve P95 ≤ 100 ms · Assign P95 ≤ 200 ms
  • PG connection pool utilisation ≤ 70 %
  • CAS conflict rate < 0.5 %

5.3 Import stress

100 k-row CSV import with concurrent ValidateLease traffic at 2000 RPS. Verify no hot-path latency regression > 20 %.


6. Security Tests

  • Role matrix: every REST endpoint × every role → 200 / 403 assertions.
  • RLS cross-tenant attack: attempt to read tenant B data while impersonating tenant A; verify zero rows returned.
  • State-race prevention: 100 concurrent Assign on same HELD identifier → exactly one succeeds, 99 CONFLICT.
  • CSV injection: malicious rows with \r\n, =HYPERLINK(...), null bytes; verify all rejected.
  • Hash-chain tampering: manual UPDATE numbering.audit SET row_hash = ... should be rejected by Postgres rule; direct raw-SQL tamper detected by daily reconciliation.
  • mTLS allowlist: client cert with non-allowlisted CN rejected with UNAUTHENTICATED.
  • Enumeration attack: sustained unknown-identifier ValidateLease traffic; verify negative-cache keeps PG load bounded.
  • ReDoS-style identifier: pathological alpha-ID pattern AAAA… rejected without backtracking (no regex catastrophic backtracking).
  • gitleaks, osv-scanner, trivy in CI.
  • Quarterly third-party pen-test scoped to numbering REST + gRPC surface.

7. End-to-End Flows

Playwright (admin-dashboard, customer-portal) + scripted gRPC:

E2E flowSteps
Full MSISDN lifecycleMNO CSV import → tenant browse → Reserve → Hold → Assign → message sent with identifier → Recall → Quarantine → 90-d time-skip → back to AVAILABLE
Short-code vanity flowAdmin marks code vanity-eligible → tenant with vanity tier reserves → Assign P1Y autoRenew=true → daily renewal runs → billing consumes → 365-d time-skip on Recall
Alpha-ID with sender-id-registryTenant submits alpha-ID KYC → sender-id-registry verifies → tenant Assign in numbering → number.assigned.v1 consumed by sender-id-registry to mark committed
Compliance suspension recallcompliance-engine publishes compliance.tenant.suspended → numbering bulk-recalls all tenant leases within 60 s
Regulator export happy path1st of month cron fires → export generated → signed → uploaded to S3 object-lock → regulator-portal notified
Multi-region failoverShut down kbl primary → mzr takes over → in-flight reservations expire cleanly; leases preserved

8. Chaos / Failure Drills

Quarterly (per FAILURE_MODES):

  • PG primary failover during active lifecycle traffic — verify < 30 s recovery, zero double-assignments.
  • Redis outage during hot-path — verify cache-miss fallback to PG, fail-closed on PG-also-down.
  • NATS outbox relay stall — verify state writes still succeed, backlog drains on recovery.
  • Cross-region network partition — verify CAS on healthy region continues; split-brain prevented.
  • MNO signing key rotation mid-import — verify grace window handles overlap.

9. Coverage & Gating

  • Unit + integration ≥ 85 % line / 80 % branch (enforced in CI).
  • Pact contracts must match 100 %.
  • Load test must pass nightly; regression > 10 % blocks release.
  • Security tests green before every production deployment.

End of TESTING_STRATEGY.md