Files
insforge--insforge/backend/tests/unit/database-migration.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

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';`);
});
});