Test Suite

Worker integration tests run in the real Cloudflare Workers runtime against an in-memory D1 database. Route coverage, response shapes, and multi-step workflows are validated before deploy.

Overview

FileTestsPurpose
api.test.ts169Every route returns the expected HTTP status code
shapes.test.ts13API response JSON matches what frontend components read
flows.test.ts25Multi-step workflows that mutate state and verify side effects
Real runtime, no mocks

Tests run inside the actual Cloudflare Workers runtime via @cloudflare/vitest-pool-workers. D1 is a real in-memory SQLite instance with all migrations applied. KV and R2 are in-memory miniflare instances. The clock is real and cannot be mocked.

Running Tests

From workers_nivaa/
bun run test         # single run
bun run test:watch   # watch mode
Deploy gate

Root ./deploy.sh worker and ./ilx-nivaa-deploy.sh worker run the worker test gate before deploy. If any test fails, the deploy is aborted. ./ilx-nivaa-verify.sh worker is the fast non-deploy equivalent, and browser-tooling scripts remain optional outside the required deploy gate.

Auth in Tests

Tests authenticate using dev headers instead of JWT. The Worker reads these headers only when ENVIRONMENT=development.

HeaderValuePurpose
x-dev-roleAdmin / Tutor / ParentSets the role for this request
x-dev-user-ide.g. 1, 100, 200Sets the authenticated user ID
x-dev-emaile.g. [email protected]Sets the email on the auth context

Seed Data

Each test file has its own beforeAll seed function. Seeds are independent — api.test.ts, shapes.test.ts, and flows.test.ts each insert their own rows and do not share state.

The api.test.ts seed creates a standard set of entities used across all endpoint tests:

EntityIDNotes
Admin userid=1Default user for admin requests
Tutor userid=100Has a tutor_profile with area, subjects, languages
Parent userid=200Has phone, address_data
Childid=1Belongs to parent 200, level=Primary 4
StudentProfileid=1Links child 1 to parent 200
Matchid=1Tutor 100 ↔ StudentProfile 1, status=Parent_Accepted
Appointmentid=1Scheduled, future date (2026-04-15)
Appointmentid=2Approved, past date (2026-03-15), 60min
PackageLedgerid=1Active, 10 sessions purchased, 8 remaining
SupportTicketid=1Open, for ticket operation tests
RescheduleRequestid=1Proposed, for decide endpoint tests
ParentIssueid=1Open, references appointment 2
ReadinessReportid=1Completed, for GET tests
Discrepancyid=1Open, references appointment 2
FraudAlertid=1Open, references match 1

api.test.ts — Endpoint Status Tests

One test per route. Asserts only the HTTP status code. Response body inspection belongs in shapes.test.ts. Grouped into describe blocks by resource.

api.test.ts Health 1 test
  • GET / 200 — returns `{ service: 'nivaa-api' }`
api.test.ts Auth 7 tests
  • GET /auth/me 200 — returns current user from dev headers
  • POST /auth/register 201 or 409 on re-run
  • POST /auth/login 401 for wrong password
  • POST /auth/demo-login 200 or 500 if demo accounts not seeded
  • POST /auth/logout 200
  • POST /auth/refresh 400 or 401 — no cookie
  • POST /auth/change-password 401 — wrong current password
api.test.ts Children 4 tests
  • GET /children/mine 200 — parent's children
  • GET /children/:id 200 — specific child
  • GET /children/:id 403/404 for wrong parent
  • GET /children/overview 200 — parent summary
api.test.ts Intake 4 tests
  • GET /intake/tutor 200 — tutor intake profile
  • POST /intake/tutor 200 or 201
  • GET /intake/child/:id 200 — child student profile
  • POST /intake/child/:id 200 or 201
api.test.ts Matches 3 tests
  • GET /matches/mine 200 — tutor's active matches
  • GET /matching/parent/proposed 200 — proposed matches for parent
  • POST /matches/:id/terminate 200 or 204 — admin terminates
api.test.ts Appointments 8 tests
  • GET /appointments/mine 200 — tutor's appointments
  • GET /appointments/mine 200 — parent's appointments
  • GET /appointments/:id 404 for nonexistent
  • POST /appointments/log 200 or 201 — tutor logs session
  • GET /appointments/:id/detail 200 or 403
  • PATCH /appointments/:id/notes 200 or 204
  • POST /appointments/:id/checkin 400 — outside time window
  • POST /appointments/:id/checkout 400 — not checked in
api.test.ts Educator Profile 4 tests
  • GET /educator/:id/profile 200 for admin
  • GET /educator/:id/profile 403 for unrelated parent
  • GET /educator/:id/profile 200 for parent with accepted match
  • GET /educator/99999/profile 404 for nonexistent
api.test.ts Tickets 7 tests
  • GET /tickets/mine 200 — user's tickets
  • POST /tickets 200 or 201 — creates ticket
  • GET /tickets/admin 200 — admin ticket list
  • POST /tickets/:id/comments 200 or 201
  • GET /tickets/:id/comments 200 — array
  • POST /tickets/:id/close 200 or 204
  • PATCH /tickets/admin/:id 200 or 204
api.test.ts Readiness Reports 5 tests
  • GET /readiness/child/:id 200 — array
  • GET /readiness/check/:id 200
  • GET /readiness/:id 200 — specific report
  • POST /readiness 200 or 201 — creates draft
  • GET /admin/readiness 200 — all reports
api.test.ts Proficiency 3 tests
  • GET /children/:id/proficiency 200 — assessment or topics
  • POST /children/:id/proficiency 200 or 201
  • GET /tutor/children/:id/proficiency 200, 403, or 404
api.test.ts Reschedule Requests 3 tests
  • GET /reschedule/mine 200 — user's requests
  • POST /reschedule 200, 201, or 409
  • POST /reschedule/:id/decide 200 or 404
api.test.ts Feedback 6 tests
  • GET /feedback/due 200 — pending review prompts
  • POST /feedback 200 or 201 — submits review
  • GET /feedback/admin/flagged 200
  • GET /feedback/admin/all 200
  • PATCH /feedback/admin/:id 200 or 404
  • GET /feedback/tutor/:id/summary 200
api.test.ts Billing 8 tests
  • GET /billing/package-summary 200
  • GET /billing/records/me 200
  • GET /billing/mtd 200
  • GET /billing/overdue-check 200
  • POST /invoices/:id/dispute 200, 201, or 404
  • GET /invoices/:id/sessions 200 or 404
  • POST /admin/billing/invoices/:id/mark-paid 200, 204, or 404
  • POST /admin/billing/invoices/:id/waive 200, 204, or 404
api.test.ts Payout 3 tests
  • GET /payout 200 — cycles list
  • GET /payout/:id/bank-status 200 or 404
  • PUT /admin/tutors/:id/bank-details 200 or 204
api.test.ts Admin — People & Students 8 tests
  • GET /admin/people 200 — users with Telegram status
  • POST /admin/people/add-parent 200, 201, or 409
  • POST /admin/people/add-coordinator 200, 201, or 409
  • GET /admin/students 200 — paginated student list
  • GET /admin/students/counts 200 — funnel counts
  • GET /admin/students/:id/recent-sessions 200
  • GET /admin/students/:id/pool-status 200 or 404
  • GET /admin/children/:id/proficiency-history 200
api.test.ts Admin — Sessions & Appointments 4 tests
  • GET /admin/appointments/operations 200
  • POST /admin/appointments/:id/attendance 200 or 201
  • POST /admin/appointments/:id/schedule-change 200 or 201
  • POST /admin/appointments/:id/flag-mismatch 200 or 201
api.test.ts Admin — Postings & Matches 6 tests
  • GET /admin/postings 200
  • GET /admin/postings/aging 200
  • GET /admin/postings/:id 200
  • POST /admin/postings/:id/close 200 or 204
  • GET /admin/matches/:id/incentive 200 or 404
  • PUT /admin/matches/:id/incentive 200 or 201
api.test.ts Admin — Billing & Packages 7 tests
  • GET /admin/package-ledgers 200
  • GET /admin/packages/renewal-queue 200
  • POST /admin/packages/:id/renew 200, 201, or 404
  • GET /admin/billing/aging 200
  • GET /admin/discounts 200
  • POST /admin/discounts 200 or 201
  • GET /admin/pricing/catalog 200
api.test.ts Admin — Tutors & Fraud 5 tests
  • GET /admin/tutors/:id/detail 200
  • PUT /admin/tutors/:id/flag 200 or 204
  • POST /admin/fraud-alerts/check 200 or 201
  • GET /admin/fraud-alerts 200
  • PATCH /admin/fraud-alerts/:id 200 or 404
api.test.ts Admin — Dashboard & Metrics 8 tests
  • GET /admin/dashboard 200
  • GET /admin/dashboard-counts 200
  • GET /admin/leaderboard 200
  • GET /admin/session-milestones 200
  • GET /admin/session-milestones/summary 200
  • POST /admin/session-milestones/:id/complete 200 or 404
  • GET /admin/metrics/monthly-hours 200
  • GET /admin/metrics/nps-summary 200
api.test.ts RBAC enforcement 3 tests
  • 5 Admin endpoints return 403 when called as Tutor
  • Admin dashboard returns 403 for Parent
  • Other parent's children return 403/404
api.test.ts Respond.io Cache 5 tests
  • GET /admin/respondio-cache/contacts 200
  • GET /admin/respondio-cache/distribution 200
  • GET /admin/respondio-cache/analytics 200
  • GET /admin/respondio-cache/contacts/:id/messages 200 or 404
  • POST /admin/respondio-cache/contacts/:id/messages/refresh 200 or 404
api.test.ts Metrics Extended 6 tests
  • GET /admin/metrics/revenue-recognition 200
  • GET /admin/metrics/margin-split 200
  • GET /admin/metrics/parent-tenure 200
  • GET /admin/metrics/daily-activity 200
  • GET /admin/metrics/adhoc-analytics 200
  • GET /admin/metrics/tutor-performance 200
api.test.ts Postings Extended 6 tests
  • GET /postings/:id 200 for seeded posting
  • POST /postings/:id/apply 200 — tutor applies to posting
  • DELETE /postings/:id/apply 200 — tutor withdraws application
  • POST /postings 201 — parent creates posting
  • POST /postings/:id/accept 200 or 404 — parent accepts application
  • POST /admin/postings/:id/push 200 or 404 — admin pushes to parent
api.test.ts Children Operations 2 tests
  • PATCH /children/:id 200 — updates display name
  • DELETE /children/:id 200 — soft-deletes child
api.test.ts Appointment Actions 2 tests
  • POST /appointments/:id/verify 200 — parent approves with `action=approve`
  • POST /appointments/:id/verify 400 — invalid action value rejected
api.test.ts Singpass 3 tests
  • GET /auth/singpass/authorize 200 — mock response when SINGPASS_CLIENT_ID not set
  • GET /auth/singpass/status 200 — returns verification status
  • GET /auth/singpass/callback 400 — missing required params
api.test.ts Telegram Endpoints 2 tests
  • POST /telegram/webhook 200/400/500 — invalid payload silently handled or rejected
  • POST /telegram/setup-webhook requires admin auth
api.test.ts Ops / ETL 3 tests
  • POST /ops/etl/timetap-import admin-only endpoint
  • POST /ops/etl/update-tutor-addresses 200 or 500 — address update runs
  • POST /ops/seed-expanded-demo 200 or 500 — demo seed runs
api.test.ts File Serve & Upload 3 tests
  • GET /upload/file/:path 404 for nonexistent file
  • POST /upload/intake 400 when not multipart/form-data
  • GET /payout/:id/csv 404 for nonexistent cycle

shapes.test.ts — Response Shape Tests

If a frontend component reads data.foo, there must be a test here asserting the API returns foo. Catches silent regressions from backend renames. Each describe block maps to a frontend page or component.

shapes.test.ts Educator Profile 8 tests
  • GET /educator/:id/profile Top-level fields: `id`, `name`, `gender`, `area`, `employment_type`, `introduction`, `joined`, `profile_photo_url`
  • GET /educator/:id/profile Arrays are arrays (never null): `languages`, `subjects`, `special_needs_experience`, `moe_syllabus`, `experience`, `education`, `certifications`, `testimonials`
  • GET /educator/:id/profile `experience_years` has `eip`, `international_school`, `moe`, `total` (number)
  • GET /educator/:id/profile `stats` has `active_matches`, `total_sessions`
  • GET /educator/:id/profile Experience entries: `role`, `organization`, `description`, `period_start`, `period_end`
  • GET /educator/:id/profile Education entries: `qualification`, `institution`, `details`, `period_start`, `period_end`
  • GET /educator/:id/profile Certifications: `name`, `issuer`, `year`
  • GET /educator/:id/profile Testimonials: `text` (non-empty string)
shapes.test.ts Tutor Points 2 tests
  • GET /tutor/points `total_points` (number), `breakdown.sessions/students/uploads/distance`
  • GET /tutor/points `rank`, `total_tutors`, `badges[]`, `monthly_history[]`, `stats.total_sessions/total_students`
shapes.test.ts Other Shapes 7 tests
  • GET /tutor/leaderboard `leaderboard[]`, `my_position`
  • GET /admin/dashboard `counts.students/educators/parents/open_postings`, `postings_needing_attention[]`
  • GET /children/mine `id` (number), `display_name` (string)
  • GET /matches/mine `child_name`, `status`
  • GET /postings/browse `id`, `subject`, `fit_score`, `location_match`
  • GET /billing/package-summary Array of package objects
  • GET /alerts/parent Structured object (type check)

flows.test.ts — Workflow Tests

Each test is a single large it() block covering a complete lifecycle. Steps are sequential; each step's result feeds the next. Direct DB access is used alongside HTTP calls to set up state that can't be reached through HTTP alone (e.g. bypassing time-window checks on check-in/out by writing timestamps directly).

flows.test.ts Session Lifecycle 3 tests
  • Full approval path: `Scheduled → Checked_In → Awaiting_Approval_Parent → Approved` — package deducted (FIFO)
  • Dispute + reversal: approve session → dispute → package session count restored, appointment unlinked
  • Future session guard: confirm endpoint rejects sessions with future date (400)
flows.test.ts Package FIFO Order 1 test
  • Two packages for same child (different `created_at`): oldest deducted first, newer untouched
flows.test.ts Posting Push Flow 1 test
  • Parent posts → tutor applies → admin pushes → parent sees blind educator cards → parent accepts → match created (`Parent_Accepted`) → posting becomes `Filled` → first-session discount created
flows.test.ts Billing Cycle 2 tests
  • Seed approved appointment → generate Draft payout cycle → finalize → fetch CSV → OCBC-format with tutor rows
  • RBAC: tutor cannot generate payout cycle (403)
flows.test.ts Exception Request 2 tests
  • Full lifecycle: tutor creates → lists → admin lists → admin approves with notes
  • Validation: invalid type → 400, empty description → 400, invalid approval status → 400
flows.test.ts Child CRUD 1 test
  • Create → update name → verify persisted → soft-delete → verify gone from list
flows.test.ts Ticket Lifecycle 1 test
  • Parent creates → parent lists → admin lists
flows.test.ts Postings 2 tests
  • Parent creates posting → sees in list
  • Tutor applies → sees in applications → withdraws → reflected immediately
flows.test.ts RBAC Enforcement 7 tests
  • Tutor blocked from 5 admin endpoints (403)
  • Parent blocked from admin dashboard (403)
  • Parent cannot see other parent's children (403/404)
  • Tutor cannot see unmatched educator profile (403)
  • Admin can see any educator profile (200)
  • Matched parent can see their tutor (200)
  • Unmatched parent blocked from tutor profile (403)
flows.test.ts Tutor Onboarding Flow 1 test
  • POST /auth/register (role=Tutor) → POST /intake/tutor with subjects, schedule, nric_name, languages → profile retrievable → tutor in admin /people list → locked fields (nric_name) immutable after first save, normal fields (area) update freely
flows.test.ts Full Parent-to-Session Lifecycle 2 tests
  • 16-step flow: create child → attempt English posting without assessment → 422 (requires_assessment: true) → complete proficiency assessment → create posting with assessment ID → tutor applies → admin sees application → admin pushes → parent sees blind card → parent accepts → verify match Parent_Accepted + posting Filled + first-session discount → seed package → DB simulate check-in/check-out → parent confirms → verify Approved, 10→9 package deduction, appointment linked to package
  • Auto-link: create child with recent Maths assessment → create posting without proficiency_assessment_id → backend auto-links existing recent assessment, returns 201 with assessment ID set

Adding Tests for New Features

  1. 1 New route: Add a status-code test to api.test.ts. Seed any required rows in the seed() function at the top of the file. If Admin-only, also add a 403 test with a lower-privileged role.
  2. 2 New response field: Add toHaveProperty assertions to the relevant describe block in shapes.test.ts. Assert the type if the frontend uses it as a specific type. Assert Array.isArray() not just toHaveProperty for arrays.
  3. 3 New multi-step workflow: Add a single it() block to flows.test.ts. Walk through each state transition via HTTP. After each mutating call, query the DB directly and assert the expected state.
  4. 4 New migration: Regenerate src/__tests__/setup.ts or the test D1 won't have the new table and every test touching it will fail with no such table.

Common Pitfalls

  • CHECK constraint violations cause 500, not 400. Look up allowed enum values in the migration before writing test body data.
  • The clock cannot be mocked in the CF Workers runtime. Bypass time-gated actions (check-in, check-out) by writing timestamps directly to the DB, then test the subsequent HTTP action.
  • Shared D1 state — all it() blocks in a file share the same database instance. Use RETURNING id and dynamic IDs rather than hardcoded ones to avoid inter-test conflicts.
  • Lowercase enums — string CHECK constraints are case-sensitive. 'trial' not 'Trial'. 'AdHoc' not 'adhoc'.
  • JSON-serialised columns — some columns store JSON strings (languages, subjects). If a shape test gets a string where it expects an array, the backend is returning raw JSON instead of parsing it.
  • FK constraints — inserts require valid foreign keys. fraud_alerts needs match_id, parent_user_id, tutor_user_id. Check the migration before seeding.
  • Role mismatches — some endpoints require Parent, not Admin. Check the route handler's requireRole() call before assuming Admin works.