Making a Personal-Finance API Trustworthy: Tests, Isolation, and Balance Accounting

Written by Rohan Nandan on September 8, 2026 · 5 min read

Article Image

Problem

Personal-finance software fails in especially costly ways. A balance can drift by cents, one user can see another user’s accounts, or derived analytics can silently become stale after an edit. Those are not cosmetic bugs: they undermine trust in money movement, reporting, and forecasts.

This project needed an API-only backend that could be trusted with:

The initial smoke tests were useful, but they were manual. They did not scale, could not run in CI, and could not reliably catch regressions. Manual checks also tend to verify the path the developer already expected, rather than the adversarial paths real users and bugs will find.

Approach

I treated API trust as a testable engineering property rather than a manual checklist.

1. Isolated integration-test architecture

The test suite uses:

Important files:

This design avoids the most common integration-test failure mode: tests contaminating each other through shared state. Every test begins from a clean schema and ends by tearing it down. It also avoids the second most common failure: accidentally testing or mutating a developer database.

2. Authentication and cross-user isolation

The auth tests verify both success and failure behavior:

Critically, foreign-resource access returns 404 rather than 403. That prevents leaking whether another user owns a particular resource ID. Category parent validation follows the same principle: a parent category belonging to another user is treated as missing.

3. Transaction accounting rules

Transactions mutate account balances, so updates and deletes require exact accounting behavior:

That last decision is deliberate. Changing a transaction’s type or account after creation could silently move money between accounts or alter its accounting meaning. The API rejects that dangerous operation instead of trying to implement it implicitly. A future transfer or type-change feature would need an explicit atomic reversal-and-apply operation, planned separately.

The balance logic lives in a small, directly testable function in backend/app/routes/transactions.py. Unit tests cover balance application and reversal, while integration tests cover the full request-to-balance behavior.

4. Derived-data invalidation

Forecasts and budgets are cached, but stale analytics are worse than slow analytics. Transaction, account, and recurring-rule mutations invalidate the affected user’s cached predictions. The cache layer is scoped by auth_user_id, so invalidation never deletes another user’s cached predictions.

The cache behavior is tested directly, including:

Stack

Results

A representative protected-endpoint flow is deliberately simple:

curl https://expense-tracker-uwrp.onrender.com/api/auth/me \
  -H 'Authorization: Bearer YOUR_ACCESS_TOKEN'

If the token is invalid, the API returns 401. If a resource belongs to someone else, the API returns 404.

How to review this work

Start with test_auth_integration.py to see the adversarial cases, then read test_crud_integration.py for balance arithmetic and ownership checks. test_forecast_budget_integration.py shows that derived analytics are not merely cached for performance; their invalidation behavior is specified and tested. Finally, test_business_logic.py isolates the smallest accounting rules from HTTP behavior.

Lessons

  1. Isolation is a feature. The most valuable test was not any happy path; it was proving that user B receives 404 for user A’s data.
  2. Make dangerous edits impossible. Immutability for transaction type and account_id avoids an entire class of balance bugs.
  3. Cache invalidation needs tests. TTLs alone are not enough. Every mutation path must be tested against the cache.
  4. Test doubles should preserve production semantics. Tests simulate Supabase user IDs with local JWTs while keeping the production verification path unchanged.
  5. Manual smoke tests do not scale. A clean, isolated integration suite turns correctness from a one-time check into a repeatable guarantee.
  6. Small pure functions help. Isolating balance arithmetic from request handling makes both the unit tests and the integration tests clearer.

Part of the "Expense Tracker Case Studies" series