Skip to main content

Patient Portal Service — Testing Strategy

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · docs/standards/TESTING_STANDARDS.md · 02 DDD

1. Coverage Targets

LayerTargetTool
Unit (domain + application)≥ 85%Vitest
Integration (adapters + DB)≥ 80%Vitest + testcontainers
Contract (BFF API)100% of public endpointsPact (consumer-driven)
E2E (patient flows)Critical paths coveredPlaywright

2. Test Layers

2.1 Unit Tests

Scope: Domain logic, use-case handlers, policy filters, proxy scope checks.

TestWhat it validates
PortalAccount state machineregister → verify → active → suspend → reinstate → close transitions
ProxyDelegation scope enforcementProxy request with scope not in delegation returns 403
ProxyDelegation validity windowExpired validTo blocks access
ResultReleasePolicy filterUnreleased observations excluded from patient response
MFA ACR checkActions requiring acr >= 2 rejected when claim insufficient
DemographicsUpdateRequestSubmit command creates entity with pending status
ExportJob idempotencySecond export request for same patient returns existing in-progress job

2.2 Integration Tests

Scope: Repository adapters, outbox relay, cache write-through, and service-to-service HTTP adapters.

TestWhat it validates
tenant-isolation.spec.ts (mandatory)Portal account for tenant A is not visible from tenant B context
outbox.spec.ts (mandatory)portal.account.created.v1 event published to NATS after account creation
inbox.spec.ts (mandatory)IDENTITY.patient.registered.v1 consumed and portal account auto-created
portal-account.repository.spec.tsCRUD against real PostgreSQL via testcontainers
proxy-delegation.repository.spec.tsDelegation queries scoped to correct tenant
access-event.repository.spec.tsAppend-only: no update/delete operations succeed
cache-adapter.spec.tsCache hit returns upstream response; miss triggers HTTP call
circuit-breaker.spec.tsUpstream failure opens circuit; degraded response returned

2.3 Contract Tests (Pact)

The patient-portal-service is the provider of its own REST surface. Consumers (web app, mobile app) generate Pact consumer contracts that are verified on every PR.

ContractProvider stateVerified
GET /v1/portal/meAccount active, MFA enabledYes
GET /v1/portal/results/labPatient has 2 released observationsYes
POST /v1/portal/appointments/requestScheduling-service availableYes
POST /v1/portal/proxy/delegationsNo existing delegationYes
POST /v1/portal/exportExport job not in progressYes

2.4 E2E Tests (Playwright)

Critical patient journeys tested against a locally-deployed stack (Docker Compose):

FlowSteps
Patient login with MFARegister → verify email → activate MFA → login → see dashboard
View lab resultsLogin → navigate to Results → lab section loads released observations
Request appointmentLogin → Appointments → Request → submits to scheduling-service stub
Grant + use proxy accessPatient grants proxy → proxy logs in → views scoped data → revoke
PHR exportRequest export → poll status → download FHIR bundle

3. Security-Specific Tests

TestValidates
Unauthenticated request → 401All /v1/portal/* endpoints reject missing JWT
Wrong tenant JWT → 403tenant_id mismatch returns 403 FORBIDDEN
Expired JWT → 401Expired token rejected
Missing SMART scope → 403INSUFFICIENT_SCOPE returned
Proxy exceeds scope → 403PROXY_SCOPE_EXCEEDED returned
Unreleased result excludedBFF never returns patient-not-visible observations
PHI absent from AI promptPrompt builder unit test asserts no PHI fields included

4. Test Data

Seed data script: test/seed/portal-seed.ts

Provides:

  • 2 tenants: tenant_a, tenant_b
  • 1 active portal account per tenant with linked patient
  • 1 proxy delegation for tenant_a (parent → minor)
  • 3 released lab observations for tenant_a patient
  • 1 upcoming appointment for tenant_a patient

5. CI Integration

StepTriggerCommand
Unit testsEvery PRpnpm test:unit
Integration testsEvery PRpnpm test:integration (requires Docker)
Contract testsEvery PRpnpm test:contract
E2E testsMerge to mainpnpm test:e2e
Coverage reportEvery PRpnpm test:coverage (fails if < 80%)