Files
insforge--insforge/backend/tests/unit/user-context.service.test.ts
wehub-resource-sync 3a28426bf4
Lint and Format Check / lint-and-format (push) Failing after 0s
Check Migrations / Check for duplicate migration numbers (push) Failing after 1s
CI Pre-merge Check / CI Pre-merge Check (push) Failing after 2m17s
chore: import upstream snapshot with attribution
2026-07-13 12:23:40 +08:00

379 lines
11 KiB
TypeScript

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<typeof vi.fn>).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<typeof vi.fn>).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<typeof vi.fn>).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',
]);
});
});