/** * Shared test fixtures for authorisation. * * Before this, `Principal` was re-declared as a literal in auth.test.ts, * records.test.ts, mutation.test.ts and half a dozen others — ten copies of the * same nine fields. Adding a field to `Principal` meant editing every one of * them, and the copies had already drifted on `scopes`, which is precisely the * field the read/write split now turns on. One factory, overridden per case. */ import type { Team, TeamRole } from '@pig/core'; import type { Database } from '@pig/db'; import type { Principal } from '../../src/lib/auth'; /** * A demand-team member with a full-scope session: the ordinary user, chosen as * the default because it is the case most tests want to vary *away* from. */ export function principal(overrides: Partial = {}): Principal { return { userId: '00000000-0000-4000-8000-000000000001', email: 'seller@example.com', name: 'Seller', isPlatformAdmin: false, teams: [{ team: 'demand', role: 'member' }], via: 'jwt', scopes: ['read', 'write'], ...overrides, }; } /** One membership, spelled out — the common override, and easy to get wrong. */ export function onTeam(team: Team, role: TeamRole): Partial { return { teams: [{ team, role }] }; } export interface FakeDatabaseOptions { /** Appended to in call order, so a test can assert what ran and in what order. */ events?: string[]; /** Rows handed to `insert().values()`, chiefly the audit activity. */ inserted?: unknown[]; /** Rows a `select()` chain resolves to. Defaults to empty. */ selected?: unknown[]; } /** * The minimum Drizzle surface `executeMutation` touches: a transaction, an * insert that records its row, and a select chain that resolves to fixed rows. * Deliberately not a database — a test that needs real SQL semantics needs a * real Postgres, and pretending otherwise is how a fake starts asserting that * broken queries work. */ export function fakeDatabase(options: FakeDatabaseOptions = {}): Database { const events = options.events ?? []; const inserted = options.inserted ?? []; const selected = options.selected ?? []; const selectChain = { from: () => selectChain, leftJoin: () => selectChain, innerJoin: () => selectChain, where: () => selectChain, orderBy: () => selectChain, limit: async () => selected, then: (resolve: (rows: unknown[]) => unknown) => resolve(selected), }; const insertChain = { values: (row: unknown) => { events.push('insert'); inserted.push(row); return { onConflictDoNothing: () => ({ returning: async () => [row] }), returning: async () => [row], then: (resolve: (value: unknown) => unknown) => resolve(undefined), }; }, }; const updateChain = { set: () => ({ where: async () => { events.push('update'); }, }), }; const tx = { select: () => { events.push('select'); return selectChain; }, insert: () => insertChain, update: () => updateChain, }; return { transaction: async (work: (transaction: unknown) => Promise) => { events.push('transaction'); return work(tx); }, select: tx.select, insert: tx.insert, update: tx.update, } as unknown as Database; }