Add PGlite-backed cross-tenant export isolation test - #232
Conversation
Seeds two tenants into a real in-process Postgres (PGlite) and invokes the account export handler as each, asserting the export returns only the requesting tenant's saved items, sources, integrations, and settings. Proves isolation at the enforcing (handler) layer rather than the query-builder layer. Closes #226
Independent code review trailRan the independent reviewer (fresh Opus, diff on stdin) over multiple rounds. The core guarantee was confirmed solid every round — dropping any of the four ownership filters in the handler fails at least one exact-array assertion. Summary of what was flagged and done: Fixed
Skipped (with reason)
|
Address independent review: add an empty-account tenant (proves empty collections/null settings, no cross-tenant findFirst leak), assert token columns and their values never reach the export, and harden spy/teardown.
Merged main + independent review trailMerged Independent Opus code review ran 3 rounds against the diff. Round 1 → applied:
Round 2 → applied:
Round 3 → applied:
Skipped, with reasons:
lint ( |
What
Adds a real Postgres (PGlite, in-process) integration test for the account export handler (
server/api/account/export.get.ts). It seeds two tenants, each with saved items, a feed, an integration, and settings, then invokes the handler as each tenant and asserts the export returns only that tenant's rows.Test file:
tests/server/api/account/export.get.isolation.test.ts(4 tests).Why
The existing
export.get.test.tsmocksuseDband pins the drizzle clauses (the query-builder layer). That proves the query is built correctly but can't prove it behaves correctly against a real database. This test closes that gap: two tenants share one real Postgres, so cross-tenant isolation is proven at the enforcing (handler) layer by actual row exclusion, not by asserting the query AST.Each ownership guard in the handler is genuinely exercised — dropping any one fails at least one assertion (verified locally):
inArray(saved-items ownership)feedsfilter (sources)integrationsfilteruserSettingsfindFirst(settings theme asserted in both directions, sincefindFirsthas no defined order)Coverage details:
publishedAt DESC NULLS LASTplusdesc(id)tiebreaker ordering.useDbspy asserted untouched).Implementation notes
tests/server/db/sourceCap.test.ts: minimal local DDL for the five tables the export reads, rather than replaying the migration chain (earlier migrations use GIN/tsvector triggers the in-memory harness doesn't reliably support). Column names mirrorserver/db/schema.ts; a drifted column fails loudly with the column named.Viewable
Not a UI change — runs via
npm run test:ci(tests/server/api/account/export.get.isolation.test.ts).Closes #226