114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { ERROR_CODES } from '@insforge/shared-schemas';
|
|
|
|
const {
|
|
connectMock,
|
|
parseSQLStatementsMock,
|
|
analyzeQueryMock,
|
|
initSqlParserMock,
|
|
checkSqlExecutionGuardsMock,
|
|
} = vi.hoisted(() => ({
|
|
connectMock: vi.fn(),
|
|
parseSQLStatementsMock: vi.fn((sql: string) => [sql]),
|
|
analyzeQueryMock: vi.fn(() => []),
|
|
initSqlParserMock: vi.fn(async () => {}),
|
|
checkSqlExecutionGuardsMock: vi.fn((query: string) =>
|
|
query.includes('RESET ROLE')
|
|
? 'Changing SQL execution role or session authorization is not allowed.'
|
|
: null
|
|
),
|
|
}));
|
|
|
|
vi.mock('../../src/infra/database/database.manager', () => ({
|
|
DatabaseManager: {
|
|
getInstance: vi.fn(() => ({
|
|
getPool: vi.fn(() => ({
|
|
connect: connectMock,
|
|
})),
|
|
})),
|
|
clearColumnTypeCache: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/utils/sql-parser', () => ({
|
|
parseSQLStatements: parseSQLStatementsMock,
|
|
analyzeQuery: analyzeQueryMock,
|
|
initSqlParser: initSqlParserMock,
|
|
checkSqlExecutionGuards: checkSqlExecutionGuardsMock,
|
|
}));
|
|
|
|
import { DatabaseMigrationService } from '../../src/services/database/database-migration.service';
|
|
|
|
describe('DatabaseMigrationService', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('blocks execution context changes before opening a database transaction', async () => {
|
|
const service = DatabaseMigrationService.getInstance();
|
|
|
|
await expect(
|
|
service.createMigration({
|
|
version: '202605020001',
|
|
name: 'role-reset',
|
|
sql: 'RESET ROLE',
|
|
})
|
|
).rejects.toMatchObject({
|
|
statusCode: 403,
|
|
code: ERROR_CODES.FORBIDDEN,
|
|
});
|
|
|
|
expect(checkSqlExecutionGuardsMock).toHaveBeenCalledWith('RESET ROLE');
|
|
expect(connectMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('reloads only the PostgREST schema cache after an allowed migration', async () => {
|
|
const queryMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce({}) // BEGIN
|
|
.mockResolvedValueOnce({}) // advisory lock
|
|
.mockResolvedValueOnce({}) // search_path
|
|
.mockResolvedValueOnce({ rows: [] }) // latest version
|
|
.mockResolvedValueOnce({}) // SET LOCAL ROLE project_admin
|
|
.mockResolvedValueOnce({}) // set local request.jwt.claims
|
|
.mockResolvedValueOnce({}) // execute migration SQL
|
|
.mockResolvedValueOnce({}) // RESET ROLE
|
|
.mockResolvedValueOnce({}) // reset request.jwt.claims
|
|
.mockResolvedValueOnce({
|
|
rows: [
|
|
{
|
|
version: '202605020002',
|
|
name: 'create-products',
|
|
statements: ['CREATE TABLE public.products (id uuid)'],
|
|
createdAt: '2026-05-02T00:00:00.000Z',
|
|
},
|
|
],
|
|
}) // insert custom_migrations row
|
|
.mockResolvedValueOnce({}) // reload schema
|
|
.mockResolvedValueOnce({}); // COMMIT
|
|
|
|
connectMock.mockResolvedValue({
|
|
query: queryMock,
|
|
release: vi.fn(),
|
|
});
|
|
|
|
const service = DatabaseMigrationService.getInstance();
|
|
const result = await service.createMigration({
|
|
version: '202605020002',
|
|
name: 'create-products',
|
|
sql: 'CREATE TABLE public.products (id uuid)',
|
|
});
|
|
|
|
expect(result.migration.version).toBe('202605020002');
|
|
expect(queryMock).toHaveBeenCalledWith('SET LOCAL ROLE project_admin');
|
|
expect(queryMock).toHaveBeenCalledWith('SELECT set_config($1, $2, $3)', [
|
|
'request.jwt.claims',
|
|
JSON.stringify({ role: 'project_admin' }),
|
|
true,
|
|
]);
|
|
expect(queryMock).toHaveBeenCalledWith('RESET ROLE');
|
|
expect(queryMock).toHaveBeenCalledWith(`NOTIFY pgrst, 'reload schema';`);
|
|
expect(queryMock).not.toHaveBeenCalledWith(`NOTIFY pgrst, 'reload config';`);
|
|
});
|
|
});
|