import { describe, expect, it, vi, beforeEach } from 'vitest'; import { withAdminContext, withUserContext, } from '../../src/services/database/user-context.service'; import type { Pool, PoolClient } from 'pg'; /** * Records every query call so the test can assert ordering and arguments. */ function makeMockClient() { const calls: Array<{ sql: string; params?: unknown[] }> = []; const client = { query: vi.fn(async (sql: string, params?: unknown[]) => { calls.push({ sql, params }); return { rows: [], rowCount: 0 } as unknown; }), release: vi.fn(), } as unknown as PoolClient; return { client, calls }; } function makeMockPool(client: PoolClient): Pool { return { connect: vi.fn(async () => client), } as unknown as Pool; } describe('withUserContext', () => { beforeEach(() => { vi.clearAllMocks(); }); it('authenticated path sets role + jwt claims inside a transaction and commits', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); const result = await withUserContext( pool, { id: 'ZVP5j6raUC9cuBIWzDGjdNdelMFjWNc5', role: 'authenticated', email: 'alice@example.com', }, async (db) => { await db.query('SELECT 1'); return 'ok'; } ); expect(result).toBe('ok'); const sequence = calls.map((c) => c.sql); expect(sequence).toEqual([ 'BEGIN', 'SET LOCAL ROLE authenticated', 'SELECT set_config($1, $2, true)', 'SELECT 1', 'COMMIT', 'RESET ROLE', ]); expect(calls[2].params?.[0]).toBe('request.jwt.claims'); expect(JSON.parse(calls[2].params![1] as string)).toEqual({ role: 'authenticated', sub: 'ZVP5j6raUC9cuBIWzDGjdNdelMFjWNc5', email: 'alice@example.com', }); expect(client.release).toHaveBeenCalledOnce(); }); it('strips the anonymous sentinel subject from database claims', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await withUserContext(pool, { id: 'anonymous', role: 'anon' }, async () => {}); // The sentinel is an API-level label: auth.uid() casts sub to uuid, so // it must never reach the database claims expect(calls[2].params?.[0]).toBe('request.jwt.claims'); expect(JSON.parse(calls[2].params![1] as string)).toEqual({ role: 'anon' }); const setLocalRole = calls.find((c) => c.sql.startsWith('SET LOCAL ROLE')); expect(setLocalRole?.sql).toBe('SET LOCAL ROLE anon'); }); it('strips anon subjects even when they are UUIDs (legacy anon JWTs)', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); // The legacy shared anon UUID never identified anyone — ownership is an // authenticated-only concept, regardless of the subject's shape await withUserContext( pool, { id: '12345678-1234-5678-90ab-cdef12345678', role: 'anon' }, async () => {} ); expect(JSON.parse(calls[2].params![1] as string)).toEqual({ role: 'anon' }); }); it('can run as project_admin when the caller wants database policies to decide', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await withUserContext( pool, { id: 'local:admin', role: 'project_admin', }, async () => {} ); expect(calls.map((c) => c.sql)).toContain('SET LOCAL ROLE project_admin'); expect(calls[2].params?.[0]).toBe('request.jwt.claims'); expect(JSON.parse(calls[2].params![1] as string)).toEqual({ role: 'project_admin', }); }); it('sets extra runtime GUCs inside the same transaction', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await withUserContext( pool, { id: 'u1', email: 'u1@example.com', role: 'authenticated' }, async (db) => { await db.query('SELECT 1'); }, { 'realtime.channel_name': 'chat:lobby', ignored: undefined, } ); expect(calls.map((c) => c.sql)).toEqual([ 'BEGIN', 'SET LOCAL ROLE authenticated', 'SELECT set_config($1, $2, true)', 'SELECT set_config($1, $2, true)', 'SELECT 1', 'COMMIT', 'RESET ROLE', ]); expect(calls[2].params?.[0]).toBe('request.jwt.claims'); expect(calls[3].params).toEqual(['realtime.channel_name', 'chat:lobby']); }); it('rejects attempts to override centralized JWT settings', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await expect( withUserContext( pool, { id: 'u1', email: 'u1@example.com', role: 'authenticated' }, async () => {}, { 'request.jwt.foo': 'evil' } ) ).rejects.toThrow(/must not override request\.jwt\.\*/); expect(calls.map((c) => c.sql)).toEqual([ 'BEGIN', 'SET LOCAL ROLE authenticated', 'SELECT set_config($1, $2, true)', 'ROLLBACK', 'RESET ROLE', ]); expect(client.release).toHaveBeenCalledOnce(); }); it('rejects mixed-case attempts to override centralized JWT settings', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await expect( withUserContext( pool, { id: 'u1', email: 'u1@example.com', role: 'authenticated' }, async () => {}, { 'Request.Jwt.Claims': '{"sub":"evil"}' } ) ).rejects.toThrow(/must not override request\.jwt\.\*/); expect(calls.map((c) => c.sql)).toEqual([ 'BEGIN', 'SET LOCAL ROLE authenticated', 'SELECT set_config($1, $2, true)', 'ROLLBACK', 'RESET ROLE', ]); expect(client.release).toHaveBeenCalledOnce(); }); it('rolls back and resets role if fn throws', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); await expect( withUserContext( pool, { id: 'u1', email: 'u1@example.com', role: 'authenticated' }, async () => { throw new Error('boom'); } ) ).rejects.toThrow('boom'); // Pin the exact sequence — flipping order or skipping RESET ROLE // silently leaks role state across the pool. expect(calls.map((c) => c.sql)).toEqual([ 'BEGIN', 'SET LOCAL ROLE authenticated', 'SELECT set_config($1, $2, true)', 'ROLLBACK', 'RESET ROLE', ]); expect(client.release).toHaveBeenCalledOnce(); }); it('always releases the client even if RESET ROLE fails', async () => { const { client, calls } = makeMockClient(); const pool = makeMockPool(client); // Make RESET ROLE fail (client.query as ReturnType).mockImplementation( async (sql: string, params?: unknown[]) => { calls.push({ sql, params }); if (sql === 'RESET ROLE') { throw new Error('reset failed'); } return { rows: [], rowCount: 0 }; } ); await withUserContext( pool, { id: 'u1', email: 'u1@example.com', role: 'authenticated' }, async () => {} ); expect(calls.map((c) => c.sql)).toContain('RESET ROLE'); expect(client.release).toHaveBeenCalledOnce(); }); }); describe('withAdminContext', () => { beforeEach(() => { vi.clearAllMocks(); }); it('uses session-level project_admin context by default and resets it', async () => { const { client, calls } = makeMockClient(); const result = await withAdminContext(client, async () => { await client.query('SELECT 1'); return 'ok'; }); expect(result).toBe('ok'); expect(calls.map((c) => c.sql)).toEqual([ 'SET ROLE project_admin', 'SELECT set_config($1, $2, $3)', 'SELECT 1', 'RESET ROLE', 'SELECT set_config($1, $2, $3)', ]); expect(calls[1].params).toEqual([ 'request.jwt.claims', JSON.stringify({ role: 'project_admin' }), false, ]); expect(calls[4].params).toEqual(['request.jwt.claims', '{}', false]); }); it('uses transaction-local project_admin context when requested', async () => { const { client, calls } = makeMockClient(); await withAdminContext( client, async () => { await client.query('CREATE TABLE public.todos (id uuid)'); }, true ); expect(calls.map((c) => c.sql)).toEqual([ 'SET LOCAL ROLE project_admin', 'SELECT set_config($1, $2, $3)', 'CREATE TABLE public.todos (id uuid)', 'RESET ROLE', 'SELECT set_config($1, $2, $3)', ]); expect(calls[1].params).toEqual([ 'request.jwt.claims', JSON.stringify({ role: 'project_admin' }), true, ]); expect(calls[4].params).toEqual(['request.jwt.claims', '{}', true]); }); it('does not swallow admin context cleanup failures', async () => { const { client, calls } = makeMockClient(); const resetError = new Error('reset failed'); const cleanupErrors: Error[] = []; (client.query as ReturnType).mockImplementation( async (sql: string, params?: unknown[]) => { calls.push({ sql, params }); if (sql === 'RESET ROLE') { throw resetError; } return { rows: [], rowCount: 0 }; } ); await expect( withAdminContext( client, async () => 'ok', false, (error) => cleanupErrors.push(error) ) ).rejects.toBe(resetError); expect(cleanupErrors).toEqual([resetError]); }); it('preserves the original SQL error when admin context cleanup also fails', async () => { const { client, calls } = makeMockClient(); const sqlError = new Error('sql failed'); const resetError = new Error('reset failed'); const cleanupErrors: Error[] = []; (client.query as ReturnType).mockImplementation( async (sql: string, params?: unknown[]) => { calls.push({ sql, params }); if (sql === 'RESET ROLE') { throw resetError; } return { rows: [], rowCount: 0 }; } ); await expect( withAdminContext( client, async () => { throw sqlError; }, false, (error) => cleanupErrors.push(error) ) ).rejects.toBe(sqlError); expect(sqlError.cause).toBe(resetError); expect(cleanupErrors).toEqual([resetError]); }); it('lets the surrounding transaction rollback clear local context after SQL failures', async () => { const { client, calls } = makeMockClient(); await expect( withAdminContext( client, async () => { await client.query('SELECT broken'); throw new Error('sql failed'); }, true ) ).rejects.toThrow('sql failed'); expect(calls.map((c) => c.sql)).toEqual([ 'SET LOCAL ROLE project_admin', 'SELECT set_config($1, $2, $3)', 'SELECT broken', ]); }); });