Skip to content

Testing ​

Every backend service has tests, and CI runs them on every pull request. The web site and the mobile app have none.

PackageFrameworkTests (2026-10-30)
apiJest + ts-jest + supertest596 unit 443 database
ogunJest + ts-jest149
anansiJest + ts-jest34
adminJest + Testing Library20
esunode:test via tsx6
igdb-heartbeatnode:test via tsx4
packages/typesJest193
packages/unfurlJest93
web—None
mobile—None

Running ​

bash
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 --forceExit

Database 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_test on every run. Set TEST_DATABASE_URL, or it points packages/database/.env's DATABASE_URL at playpals_test on the same server.
  • It refuses any database whose name doesn't end in _test.
  • @playpals/db is the real package here, so run pnpm build in packages/database after 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.

WhereWhatRun
apps/api/__tests__/db/finance/moneyRules.dbtest.tsThe 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 ratepnpm test:db
…/finance/monthEndToEnd.dbtest.tsOne 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 payoutpnpm test:db
…/finance/payoutRun.dbtest.tsSeveral 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 windowpnpm test:db
…/finance/reversals.dbtest.tsFull and partial transfer reversals, repaying once, the USD ledger after a reversal, and reconciliation against Stripepnpm test:db
apps/api/__tests__/stripe-sandbox/payouts.sandbox.tsThe 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 balancepnpm 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_bypassPending test 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).
bash
cd apps/api
STRIPE_SANDBOX_SECRET_KEY=sk_test_… pnpm test:stripe-sandbox
# in another terminal, to watch every request:
stripe logs tail

Still 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.ts

The 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.ts

The 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:

js
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.

ts
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:

ts
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:

  1. lib/roomAccess.ts — canAccessRoom is the channel paywall. A bug here gives away paid content. Four visibility branches, all pure logic with mockable Prisma calls.
  2. middleware/authenticated.ts — token missing, expired, invalid, user deleted, tokenVersion mismatch. Five branches guarding everything.
  3. modules/revenuecat/ — idempotency, product mapping, each event type. Money.
  4. services/feed.ts — cache key construction, cursor handling, invalidation.
  5. 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 ​

bash
cd apps/<service>
pnpm add -D jest ts-jest @types/jest

Copy 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.

Internal documentation — PlayPalz platform