69 lines
3.5 KiB
TypeScript
69 lines
3.5 KiB
TypeScript
import { Pool } from 'pg';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { PostgresAssessorRepository } from '../../server/postgres/postgres-assessor-repository';
|
|
import { createPostgresReportReadRuntime, REPORT_READ_ONLY_SESSION_OPTIONS } from '../../server/reporting/report-read-runtime';
|
|
|
|
afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); });
|
|
|
|
function fakePool(readOnly: 'on' | 'off') {
|
|
const statements: string[] = [];
|
|
const pool = {
|
|
query: vi.fn(async (text: string) => {
|
|
statements.push(text);
|
|
if (text === 'SHOW default_transaction_read_only') return { rows: [{ default_transaction_read_only: readOnly }] };
|
|
if (text.startsWith('SELECT revision')) return { rows: [{ revision: '7' }] };
|
|
throw new Error(`unexpected statement: ${text}`);
|
|
}),
|
|
connect: vi.fn(async () => { throw new Error('a read-only reader must not open a transaction'); })
|
|
};
|
|
return { pool: pool as unknown as Pool, statements };
|
|
}
|
|
|
|
describe('Report Engine read-only database reader', () => {
|
|
it('opens only on a read-only session, runs only SELECT/SHOW and schedules no Result DB rebuild', async () => {
|
|
vi.useFakeTimers();
|
|
const rebuild = vi.spyOn(PostgresAssessorRepository.prototype as unknown as { scheduleResultDbRebuild: () => void },
|
|
'scheduleResultDbRebuild');
|
|
const { pool, statements } = fakePool('on');
|
|
const repository = await PostgresAssessorRepository.openReadOnly(pool);
|
|
expect(repository.revision).toBe(7);
|
|
vi.runAllTimers();
|
|
expect(rebuild).not.toHaveBeenCalled();
|
|
expect(vi.getTimerCount()).toBe(0);
|
|
expect(statements.every(text => /^(?:SHOW|SELECT)\b/.test(text))).toBe(true);
|
|
});
|
|
|
|
it('refuses a session that could write', async () => {
|
|
const { pool } = fakePool('off');
|
|
await expect(PostgresAssessorRepository.openReadOnly(pool)).rejects.toThrow(/default_transaction_read_only=on/);
|
|
});
|
|
|
|
it('configures its own pool with read-only sessions and a distinct application name', async () => {
|
|
const created: Array<Record<string, unknown>> = [];
|
|
vi.spyOn(PostgresAssessorRepository, 'openReadOnly').mockImplementation(async pool => {
|
|
created.push((pool as unknown as { options: Record<string, unknown> }).options);
|
|
throw new Error('stop after configuration');
|
|
});
|
|
await expect(createPostgresReportReadRuntime({ ASSESSOR_DATABASE_URL: 'postgresql://dummy:dummy@127.0.0.1:5432/assessor' }))
|
|
.rejects.toThrow('stop after configuration');
|
|
expect(created[0]).toMatchObject({ options: REPORT_READ_ONLY_SESSION_OPTIONS, application_name: 'assessor-report-reader', max: 2 });
|
|
});
|
|
});
|
|
|
|
const connectionString = process.env.ASSESSOR_TEST_DATABASE_URL;
|
|
describe.skipIf(!connectionString)('Report Engine read-only reader against a PostgreSQL test database', () => {
|
|
it('lets PostgreSQL reject a write attempt through the report reader', async () => {
|
|
const runtime = await createPostgresReportReadRuntime({ ASSESSOR_DATABASE_URL: connectionString });
|
|
const pool = new Pool({ connectionString, options: REPORT_READ_ONLY_SESSION_OPTIONS, max: 1 });
|
|
try {
|
|
expect(await runtime.port.currentRevision()).toBeGreaterThanOrEqual(0);
|
|
// WHERE false: even an accepted statement could not change a row; PostgreSQL still rejects it.
|
|
await expect(pool.query('UPDATE assessor_repository_state SET revision = revision WHERE false'))
|
|
.rejects.toMatchObject({ code: '25006' });
|
|
} finally {
|
|
await pool.end();
|
|
await runtime.close();
|
|
}
|
|
});
|
|
});
|