Testing
Every backend service has tests, and CI runs them on every pull request. The web site and the mobile app have none.
| Package | Framework | Tests (2026-10-30) |
|---|---|---|
api | Jest + ts-jest + supertest | 596 unit 443 database |
ogun | Jest + ts-jest | 149 |
anansi | Jest + ts-jest | 34 |
admin | Jest + Testing Library | 20 |
esu | node:test via tsx | 6 |
igdb-heartbeat | node:test via tsx | 4 |
packages/types | Jest | 193 |
packages/unfurl | Jest | 93 |
web | — | None |
mobile | — | None |
Running
cd apps/ogun && pnpm test
cd apps/anansi && pnpm test
cd apps/api && pnpm test # unit tests; @playpals/db is mocked
pnpm test:db # against a real Postgres (see below)
pnpm test:stripe-sandbox # anansi against a real Stripe sandbox (opt-in, below)
pnpm test:watch
pnpm test:coverage
pnpm test:ci # --ci --coverage --forceExitDatabase tests
Some guarantees exist only in Postgres: the coin ledger's locking, triggers and constraints. pnpm test:db runs apps/api/__tests__/db/**/*.dbtest.ts against a real database.
- It drops, recreates and migrates
playpals_teston every run. SetTEST_DATABASE_URL, or it pointspackages/database/.env'sDATABASE_URLatplaypals_teston the same server. - It refuses any database whose name doesn't end in
_test. @playpals/dbis the real package here, so runpnpm buildinpackages/databaseafter changing it.- Docker's Postgres must be up (
docker-compose up -d).
Financial suite
Run before every release that touches money. It checks that VIP passes, 1-on-1 sessions, e-dates and coin tips turn into exactly the right PlayPal earnings, and that anansi pays them once. It also covers refunds, failed transfers and Stripe reversals, and the books have to balance throughout.
| Where | What | Run |
|---|---|---|
apps/api/__tests__/db/finance/moneyRules.dbtest.ts | The money rules as fixed values, worked out independently: every price tier at Apple 30%, Apple 15% and Google with tax. Also a sweep of every price from 1¢ to $1,000 across tax, commission and fee rates against exact integer arithmetic, and the coin cash-out rate | pnpm test:db |
…/finance/monthEndToEnd.dbtest.ts | One PlayPal's month through the real webhook, booking and tip code. Covers two VIP passes and a refunded one; sessions held, cancelled early and late, and no-shows on both sides; an e-date; and cashed-out tips. Then maturation, then anansi's calculate and transfer. Every amount is asserted to the cent, along with the Stripe calls, the USD ledger, no double pay on a re-run, and a refund after payout | pnpm test:db |
…/finance/payoutRun.dbtest.ts | Several PlayPals: not set up, under $1.00, a failed transfer and its retry, overlapping runs, a second payout in a month, the balance check, and a retry after the 24-hour idempotency window | pnpm test:db |
…/finance/reversals.dbtest.ts | Full and partial transfer reversals, repaying once, the USD ledger after a reversal, and reconciliation against Stripe | pnpm test:db |
apps/api/__tests__/stripe-sandbox/payouts.sandbox.ts | The scenarios from the Stripe testing guide against real Stripe test mode: happy path, no double pay, an account that isn't ready, reversal plus reconciliation, the run lock, and too little balance | pnpm test:stripe-sandbox |
fixtures.ts in the finance folder holds the builders (readyPlaypal, buyVipPass, bookAndPay, tipPlaypal, requestCashout, reverseTransfer, expectBooksBalanced, …). anansi's services run against the test database by path. Each file stubs anansi's Redis lock, Stripe client and logger.
The Stripe sandbox suite
It skips itself unless STRIPE_SANDBOX_SECRET_KEY is set, and refuses anything but an sk_test_ or rk_test_ key. Use a dedicated Stripe Sandbox (for example "playpalz-dev"), not the live account's test mode.
- The key needs Connect accounts write, Transfers write, Top-ups write, Balance read and PaymentIntents write.
- The suite funds the balance with the
pm_card_bypassPendingtest card. It creates custom connected accounts with Stripe's test identity values and deletes them afterwards. - Redis must be up (the run lock is real here).
cd apps/api
STRIPE_SANDBOX_SECRET_KEY=sk_test_… pnpm test:stripe-sandbox
# in another terminal, to watch every request:
stripe logs tailStill manual before launch: confirm Top-ups are enabled on the live account, and walk one real PlayPal through Accounts v2 onboarding in the sandbox (payoutsEnabled set by the webhook).
What is covered
ogun
src/__tests__/
├── jobs/processMedia.test.ts
├── jobs/processImage.test.ts
├── jobs/processVideo.test.ts
├── utils/helper.test.ts
├── utils/redis.test.ts
└── utils/storage.test.tsThe media pipeline is the best-tested part of the platform — sensible, since it is the most complex pure-logic code in the repo and the hardest to verify by hand.
anansi
__tests__/unit/services/
├── payoutRun.test.ts
└── earnings.test.tsThe payout run is also exercised end to end, against a real database, by the financial suite.
Also the right priority: the earnings calculation and payout orchestration are where a bug costs money.
The API's unit test setup
apps/api/__tests__/ holds unit tests (unit/: controllers, services, middleware, lib, jobs, schemas), route tests (integration/routes/) and the database suites (db/, run by pnpm test:db).
The unit configuration:
moduleNameMapper: {
"^@playpals/db$": "<rootDir>/__tests__/__mocks__/@playpals/db.ts",
},
setupFiles: ["<rootDir>/__tests__/setup.ts"],
clearMocks: true,
restoreMocks: true,
coverageThreshold: {
global: { branches: 50, functions: 50, lines: 60, statements: 60 },
},The @playpals/db module mapper is the important piece — it swaps Prisma for a mock, so unit tests need no database. Infrastructure modules (logger, upload, mux, livekit, redis, server, configs) are excluded from coverage, so the thresholds measure business logic rather than glue.
dev-notes/api-jest-testing-plan.md and dev-notes/api-jest-implementation-guide.md contain the intended plan.
Writing tests
Unit — a service
Services are the highest-value target: pure logic, database mocked.
import { prisma } from "@playpals/db"; // resolves to the mock
import * as sessionService from "../../../src/services/session";
jest.mock("@playpals/db");
describe("createSession", () => {
it("rejects a booking outside the creator's availability", async () => {
(prisma.availability.findMany as jest.Mock).mockResolvedValue([]);
await expect(
sessionService.createSession({ playpalId: "c1", purchaserId: "u1", /* … */ }),
).rejects.toThrow();
});
});Integration — a route
supertest exercises the full middleware stack:
import request from "supertest";
import app from "../../../src/app";
describe("GET /api/v1/feed", () => {
it("401s without a token", async () => {
await request(app).get("/api/v1/feed").expect(401);
});
});Where to start
If you are adding tests, take them in this order — highest risk first:
lib/roomAccess.ts—canAccessRoomis the channel paywall. A bug here gives away paid content. Four visibility branches, all pure logic with mockable Prisma calls.middleware/authenticated.ts— token missing, expired, invalid, user deleted,tokenVersionmismatch. Five branches guarding everything.modules/revenuecat/— idempotency, product mapping, each event type. Money.services/feed.ts— cache key construction, cursor handling, invalidation.services/notify.ts— preference gating, target building.
Each is a contained, high-consequence unit that the existing mock setup already supports.
What is not tested anywhere
esu— socket handlers, authorization callbacks, presence. All logic, all untested.- Mobile — no test setup at all. React Native Testing Library plus a mocked query client would cover hooks and screens.
- End-to-end — nothing exercises a full flow (register → post → process → appear in feed).
Adding tests to a package without them
cd apps/<service>
pnpm add -D jest ts-jest @types/jestCopy apps/ogun/jest.config.js as a starting point, and mirror the @playpals/db mock from apps/api/__tests__/__mocks__/ if the service touches the database.
CI
.github/workflows/ci.yml runs lint, types and every affected package's tests on each pull request and push to master. When api or one of its packages changed, it also runs the API's database tests against Postgres 15. See Continuous Integration.
