archive
test strategy.md
A frozen record of a decision at the time. Superseded by a new record rather than edited.
Written by the build · 2 September 2026
****# Bundul Backend — Test Strategy
Stack: NestJS · MongoDB · GraphQL · Passport/CPX · Plaid ****
Current State
14 spec files exist. No coverage baseline. Integration and e2e configs exist but are largely empty. No contract tests. No staging automation. The test infrastructure (in-memory MongoDB, fixtures, helpers) is already in place and usable.
Testing Layers
Layer 1 — Unit Tests
What: Business logic in isolation. All external dependencies mocked.
Runs: CI on every PR. Target < 60s total.
Priority targets:
| Service | Logic to cover |
|---|---|
PaymentOrchestrationService |
Fee calculation, discount application, utility subscription exclusion (emailAccessId + hasInvoice checks), split payment fee |
PaymentRetryService |
Retry guard conditions — wrong type, already scheduled, max attempts |
SubscriptionService |
subsNotPaidForInCurrentMonth filtering, split payment option calculation, due date range |
SmartDueDateService |
Plaid stream filtering (active, monthly, predicted date, amount > $2), Claude prompt construction, JSON parsing, date validation (future dates only), fallback when AI returns fewer than 5 suggestions |
DiscountService |
Code validation, default fallback, expiry |
| All 16 resolvers | Guard enforcement, DTO mapping, error propagation |
Tooling: Jest + @nestjs/testing provider mocks + jest.fn()
Layer 2 — Integration Tests
What: Multi-model DB operations against real Mongoose with in-memory MongoDB. No external HTTP calls.
Runs: CI on every PR. Target < 3 minutes total.
Priority scenarios:
| Scenario | What it verifies |
|---|---|
deleteMyAccount cascade |
All 12 dependent models (UserSubscription, PlaidToken, BackgroundJob, etc.) are deleted for that user only |
| Idempotency key deduplication | Calling processPayment twice with the same key creates exactly one payment record |
| BackgroundJob state machine | PENDING → PROCESSING → COMPLETED and PENDING → PROCESSING → FAILED → retry enqueued |
| Subscription lifecycle | create → activate → hasPaidForThisMonth = true → cancel |
| Orphaned record cleanup | Seeded orphan records across all models → cleanup mutation → assert only orphans removed |
| Webhook → subscription update | Feed real webhook fixture payload → assert correct model fields updated |
Tooling: Existing createTestModule() + getTestMongoUri() helpers + mongodb-memory-server
Layer 3 — Contract Tests
What: Verify that external service integrations handle real API response shapes correctly. Uses recorded HTTP responses — no live network in CI.
Runs: CI on every PR.
How it works: Run once against real Passport/Plaid sandbox in record mode → save responses as fixtures → all future test runs replay from fixtures via nock (HTTP interceptor). Sandbox rate limits and availability are irrelevant after the initial recording.
Scenarios to record:
| Service | Scenarios |
|---|---|
| Passport/CPX | Charge success, charge failed, insufficient funds, card-ready webhook, card-settled webhook |
| Plaid | syncTransactions success, getRecurringTransactions, empty result, 429 rate limit |
| Airtable | getBundulPricing success, missing Bundul Fee key |
Tooling: nock + existing test/fixtures/ directory
Layer 4 — Staging Scenario Tests
What: Full-stack read verification against a dedicated test account on the staging environment. Tests the complete data pipeline — Plaid → recurring detection → subscription matching → pricing — with real connected account data.
Runs: Nightly. Not on PRs — staging availability, test account contention, and network latency make PR builds non-deterministic.
Test account requirements:
- Plaid bank account connected
- Passport/CPX card issued
- ≥1 active subscription
- Existing payment history
What gets tested:
| Query / Mutation | What it verifies |
|---|---|
getUserProfile |
Account data loads correctly |
getUserDataVersion |
Version counters are present and numeric |
getUserPaymentDetails |
Payment records load correctly |
getSupportedSubscriptionsForUser |
Full pricing pipeline with real discount state |
getUserOneSubDueDateRange |
Returns 5 AI suggestions with date + explanation fields; suggestions array present in response; falls back to heuristic when AI fails |
getUserVirtualCard |
Passport card data is retrievable |
| Write mutations (e.g. update due date) | Assert state change → restore original (explicit teardown) |
Tooling: Axios GraphQL client + Jest + SMOKE_TEST_JWT env var
Layer 5 — Full Payment E2E
What: The complete async payment chain: processPayment → Passport charge → BackgroundJob created → Passport webhook fires → subscription status updated.
Runs: Weekly or manually before major releases. Not on PRs — async webhook timing is non-deterministic, Passport sandbox has quota limits, and each run adds 5–10 minutes.
What it covers that other layers don't:
- The webhook endpoint is reachable from Passport's servers
- The full async chain completes end-to-end with real timing
- Subscription status reflects the correct final state after settlement
Tooling: Passport sandbox account + staging environment + manual trigger
Coverage Map
| Risk | Layer responsible |
|---|---|
| Fee calculation wrong | Unit |
| Utility sub incorrectly included in payment | Unit |
| Account deletion doesn't cascade | Integration |
| Duplicate payment charged | Integration (idempotency) |
| Passport response shape changes | Contract |
| Plaid returns unexpected empty response | Contract |
| Real user's subscription state is wrong | Staging scenario |
| Webhook never reaches backend | Payment E2E |
| Full payment chain breaks end-to-end | Payment E2E |
What Is Not Automated
| Operation | Reason | Approach |
|---|---|---|
| OTP delivery (Infobip SMS) | Cannot assert receipt of real SMS in CI | Unit test that sendSms is called with correct params; manual verification with team phone number once per environment setup |
cleanupOrphanedUserRecords in production |
Destructive, requires human review before execution | Covered fully in integration tests; manual execution only in production |
| First-time user payment (fresh account) | Requires a disposable test account | Covered by Layer 5 using a throwaway staging account |
Phased Rollout
| Phase | Work | Confidence gained |
|---|---|---|
| 1 | Record Passport + Plaid sandbox responses; extend webhook fixtures | Unblocks all contract tests |
| 2 | Unit tests for payment orchestration and retry logic | Regression safety on payment logic |
| 3 | Integration tests for cascade delete, idempotency, job state machine | Confidence on the riskiest DB operations |
| 4 | Set up staging test account + reset mutation + nightly run | Full-stack verification with real connected account data |
| 5 | Audit and fix existing 14 spec files | Close gaps in current coverage baseline |
| 6 | Payment E2E on staging + schedule weekly run | End-to-end production parity |
This is a record of a decision at the time. It is not edited — write a new record that supersedes it.