Assessor_2.0/tests/postgres/report-read-only-runtime.te...

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();
}
});
});