Skip to main content

Backend testing

Status: Scaffolded 2026-09-24

vitest runs the suite, supertest drives the Express app returned by buildApp, and mongodb-memory-server provides a real MongoDB per run. No database mocks: schema validation, unique indexes, TTL fields, and 2dsphere queries all execute for real, so the suite proves the data layer and not a stand-in for it.

Layout​

test/
helpers/
app.ts buildTestApp(): buildApp with a test env and a silent logger
db.ts start and stop the memory server, drop the database between files
auth.ts createUser(), asMember(app), asAdmin(app): register and return a Bearer token
routes/
auth.test.ts
places.test.ts
healthz.test.ts
stats.test.ts
services/
cursor.test.ts
crypto.test.ts
vitest.config.ts globalSetup starts one memory server, each test file gets its own database name

Route tests sit under test/routes/ and are named after the router file they cover. Pure functions in lib/ and services/ get unit tests under test/services/. Fixtures are built in the test through the API where possible (register a user, create a place) so a test exercises the same path a client would.

What every route test includes​

CaseAssert
Happy pathstatus, the envelope shape, and that the response contains only the fields the API documents
Validation error400, error.code === 'validation_error', and a details[].path naming the bad field
Unauthenticated401 unauthenticated with no token, and with an expired or garbage token
Wrong principal or rolea second user, or a member on an admin route: 403, or 404 where existence must not leak
Visibility filteringcreate items the caller may and may not see, list, and assert the hidden ones are absent

The last two cases are the permission boundary. A route test without them is incomplete regardless of what else it covers. For places specifically: a pending place never appears in GET /api/places for a member, does appear in GET /api/admin/places/pending for an admin, and returns 404 from GET /api/places/:slug until approved.

Example shape:

it('hides pending places from the public list', async () => {
const admin = await asAdmin(app);
const member = await asMember(app);
await member.post('/api/places').send(validPlace).expect(201);
const res = await request(app).get('/api/places?bbox=-119,33,-117,35').expect(200);
expect(res.body.items).toHaveLength(0);
await admin.get('/api/admin/places/pending').expect(200);
});

Running​

npm test # the whole suite, what validate runs
npx vitest run test/routes/places.test.ts # one file
npx vitest test/routes/places # watch mode on a pattern
npx vitest run -t 'hides pending' # one test by name

Tests log nothing (LOG_LEVEL is forced to silent under NODE_ENV=test). Set LOG_LEVEL=debug explicitly when chasing a failure.

First run​

mongodb-memory-server downloads a MongoDB binary (around 100 MB) into its cache directory the first time it runs on a machine, and again when the pinned version changes. On a slow connection the first test file can exceed the hook timeout; vitest.config.ts sets hookTimeout high enough for the download, and CI caches the binary directory keyed on the package lock so it downloads once per version. If the first run fails with a download error, run it again; a partial download is retried, not reused.

On Windows the binary is cached per user, and antivirus scanning of the extracted binary can add a few seconds to startup. Nothing in the tests depends on the platform.

What the scaffold suite covers​

Auth (register, login, magic-link issue and verify, sessions list and revoke, me, delete), places (bbox list, get by slug, create pending, admin approve and reject), healthz, stats, and the cursor and crypto helpers. The 501 stubs have one test each asserting the status and code, so a route that is accidentally left unmounted fails.