Integration tests run against a fake data layer; RLS stays with Postgres
Affects: backend/test/api-integration.test.ts, backend/test/support/, backend/package.json, .github/workflows/ci.yml
The decision
Two tiers, and the split is on what each can honestly prove.
The HTTP suite boots the real Express app on an ephemeral port and drives it with fetch, against an in-memory stand-in for Prisma. It runs anywhere in about six seconds, with no database. It covers what a controller-level test structurally cannot: helmet, CORS, the body-size limit, the rate limiters, Express's own routing and 404 handling, and every middleware in the chain in the order it actually runs. Two of the defects it was written to catch — a malformed query string answering 500 instead of 422, and a malformed JSON body doing the same — were live in main and invisible to every existing test, because neither fault is inside a controller.
Row-level security is deliberately not in that suite. RLS is enforced by Postgres. A fake client would report every policy as passing with every policy dropped, and a fake that pretended to enforce tenancy would be worse than no test at all, because the reassurance would be false. That proof belongs to test:rls against a real postgres:16 service container, and nowhere else.
Why a fake rather than a container for the HTTP tier
Environmental necessity, not preference. The development sandbox has no Postgres binary, no Docker, no root, and cannot reach the Prisma engine download host. A suite that could only run in CI would not be run while the code was being written, which is when it is worth the most.
The coverage floor
test:coverage fails under 90% lines, 80% branches, 90% functions across src. Current figures are 98.8 / 90.8 / 93.3.
The gap is intentional. A gate set exactly at today's number fails on ordinary drift — someone adds an early return, coverage moves a tenth of a point, the build goes red for nothing, and the reliable next step is that somebody lowers the threshold. Set below, it fires on a real regression and stays quiet otherwise. Raising it is a deliberate act; letting it slip is not something a pull request should be able to do quietly.
Coverage is evidence that the tests execute the code. It is not evidence that the assertions are worth anything, and it should not be read as such.