Bundul
Internal
Browse docs
Waiting for review

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.