Route every transactional outbox write through one complete event-envelope helper so occurred_at is always populated. Separate employee position and profession lookup parameters to avoid PostgreSQL 42P08, and add unit plus live-database regression coverage.
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildApp } from '../../src/app.js';
|
|
import { loadConfig } from '../../src/config.js';
|
|
import { newUlid } from '../../src/core/ids.js';
|
|
import { insertOutboxEvent } from '../../src/core/outbox.js';
|
|
import { Database } from '../../src/infrastructure/database.js';
|
|
|
|
const connectionString = process.env.DATABASE_TEST_URL;
|
|
const describeDatabase = connectionString ? describe : describe.skip;
|
|
|
|
describeDatabase('live PostgreSQL audit remediations', () => {
|
|
it('creates an employee and completes the outbox-producing shift and payroll workflow', async () => {
|
|
const database = new Database(connectionString!);
|
|
const tenantId = newUlid();
|
|
const actorId = newUlid();
|
|
const managerToken = 'm'.repeat(64);
|
|
const orgUnitId = newUlid();
|
|
const sessionId = newUlid();
|
|
const currentDatabase = await database.pool.query<{ name: string }>(
|
|
'SELECT current_database() AS name',
|
|
);
|
|
expect(currentDatabase.rows[0]?.name).toMatch(/(?:test|audit)/i);
|
|
const probeOutboxId = await database.tenantTransaction(tenantId, actorId, (client) =>
|
|
insertOutboxEvent(client, {
|
|
tenantId,
|
|
eventType: 'staff.audit_probe.created',
|
|
subjectType: 'AuditProbe',
|
|
subjectId: newUlid(),
|
|
actorType: 'user',
|
|
actorId,
|
|
payload: { source: 'live-postgres-test' },
|
|
correlationId: newUlid(),
|
|
}),
|
|
);
|
|
const role = await database.pool.query(
|
|
'SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user',
|
|
);
|
|
expect(role.rows[0]).toEqual({ rolsuper: false, rolbypassrls: false });
|
|
const probe = await database.tenantTransaction(tenantId, actorId, (client) =>
|
|
client.query<{ occurred_at: Date }>('SELECT occurred_at FROM staff_outbox WHERE id = $1', [
|
|
probeOutboxId,
|
|
]),
|
|
);
|
|
expect(probe.rows[0]?.occurred_at).toBeTruthy();
|
|
|
|
const app = await buildApp({
|
|
config: loadConfig({
|
|
DATABASE_URL: connectionString!,
|
|
STANDALONE_TENANT_ID: tenantId,
|
|
STANDALONE_ACTOR_ID: actorId,
|
|
STANDALONE_MANAGER_TOKEN: managerToken,
|
|
STANDALONE_TERMINAL_TOKEN: 't'.repeat(64),
|
|
STANDALONE_ACCESS: 'manager',
|
|
LOG_LEVEL: 'silent',
|
|
NODE_ENV: 'test',
|
|
}),
|
|
database,
|
|
});
|
|
const authorization = { authorization: `Bearer ${managerToken}` };
|
|
try {
|
|
const employee = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/staff/employees',
|
|
headers: authorization,
|
|
payload: {
|
|
personnel_number: `AUDIT-${newUlid()}`,
|
|
display_name: 'Тестовый сотрудник',
|
|
position: 'Тестовая профессия',
|
|
},
|
|
});
|
|
expect(employee.statusCode).toBe(201);
|
|
const employeeId = employee.json<{ id: string }>().id;
|
|
|
|
await database.tenantTransaction(tenantId, actorId, async (client) => {
|
|
await client.query(
|
|
`INSERT INTO staff_org_units (id, tenant_id, code, name)
|
|
VALUES ($1, $2, $3, 'Тестовая точка')`,
|
|
[orgUnitId, tenantId, `audit-${newUlid().toLowerCase()}`],
|
|
);
|
|
await client.query(
|
|
`INSERT INTO staff_work_sessions
|
|
(id, tenant_id, employee_id, org_unit_id, business_date, started_at, ended_at,
|
|
worked_minutes, status)
|
|
VALUES ($1, $2, $3, $4, '2040-01-15', '2040-01-15T01:00:00Z',
|
|
'2040-01-15T09:00:00Z', 480, 'pending_review')`,
|
|
[sessionId, tenantId, employeeId, orgUnitId],
|
|
);
|
|
});
|
|
|
|
const confirmed = await app.inject({
|
|
method: 'PATCH',
|
|
url: `/api/v1/staff/work-sessions/${sessionId}`,
|
|
headers: authorization,
|
|
payload: { status: 'confirmed' },
|
|
});
|
|
expect(confirmed.statusCode).toBe(200);
|
|
|
|
const calculated = await app.inject({
|
|
method: 'POST',
|
|
url: '/api/v1/staff/payroll/calculate',
|
|
headers: authorization,
|
|
payload: { month: '2040-01' },
|
|
});
|
|
expect(calculated.statusCode).toBe(200);
|
|
const runId = calculated.json<{ run_id: string }>().run_id;
|
|
|
|
const approved = await app.inject({
|
|
method: 'POST',
|
|
url: `/api/v1/staff/payroll/runs/${runId}/approve`,
|
|
headers: authorization,
|
|
payload: {},
|
|
});
|
|
expect(approved.statusCode).toBe(200);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
});
|