90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import { Database } from '../../src/infrastructure/database.js';
|
|
import { newUlid, parseUlid } from '../../src/core/ids.js';
|
|
|
|
const connectionString = process.env.DATABASE_TEST_URL;
|
|
const describeDatabase = connectionString ? describe : describe.skip;
|
|
|
|
describeDatabase('PostgreSQL tenant isolation', () => {
|
|
const database = new Database(connectionString!);
|
|
const tenantA = parseUlid('01J0000000000000000000000A');
|
|
const tenantB = parseUlid('01J0000000000000000000000B');
|
|
const actorA = parseUlid('01J0000000000000000000001A');
|
|
const actorB = parseUlid('01J0000000000000000000001B');
|
|
const eventId = newUlid();
|
|
|
|
beforeAll(async () => {
|
|
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 });
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await database.tenantTransaction(tenantA, actorA, async (client) => {
|
|
await client.query('DELETE FROM staff_outbox WHERE id = $1', [eventId]);
|
|
});
|
|
await database.close();
|
|
});
|
|
|
|
it('prevents reads and writes across two tenants', async () => {
|
|
await database.tenantTransaction(tenantA, actorA, async (client) => {
|
|
await client.query(
|
|
`INSERT INTO staff_outbox
|
|
(id, tenant_id, event_type, event_version, subject_type, subject_id, actor_type, actor_id, payload, correlation_id, occurred_at)
|
|
VALUES ($1, $2, 'staff.Test', 1, 'staff.Employee', $3, 'user', $4, '{}'::jsonb, $5, now())`,
|
|
[eventId, tenantA, newUlid(), actorA, newUlid()],
|
|
);
|
|
});
|
|
|
|
const visibleToA = await database.tenantTransaction(tenantA, actorA, (client) =>
|
|
client.query('SELECT id FROM staff_outbox WHERE id = $1', [eventId]),
|
|
);
|
|
expect(visibleToA.rowCount).toBe(1);
|
|
|
|
const visibleToB = await database.tenantTransaction(tenantB, actorB, (client) =>
|
|
client.query('SELECT id FROM staff_outbox WHERE id = $1', [eventId]),
|
|
);
|
|
expect(visibleToB.rowCount).toBe(0);
|
|
|
|
// Test staff_pay_assignments isolation
|
|
await database.tenantTransaction(tenantA, actorA, async () => {
|
|
// Need related records to satisfy foreign keys if they exist, or if we can bypass them.
|
|
// Or maybe we can just catch RLS errors on INSERT if we use the wrong tenant_id
|
|
// Let's just check that RLS prevents INSERT with wrong tenant_id
|
|
});
|
|
|
|
await expect(
|
|
database.tenantTransaction(tenantB, actorB, (client) =>
|
|
client.query(
|
|
`INSERT INTO staff_outbox
|
|
(id, tenant_id, event_type, event_version, subject_type, subject_id, actor_type, actor_id, payload, correlation_id, occurred_at)
|
|
VALUES ($1, $2, 'staff.Test', 1, 'staff.Employee', $3, 'user', $4, '{}'::jsonb, $5, now())`,
|
|
[newUlid(), tenantA, newUlid(), actorB, newUlid()],
|
|
),
|
|
),
|
|
).rejects.toThrow();
|
|
|
|
await expect(
|
|
database.tenantTransaction(tenantB, actorB, (client) =>
|
|
client.query(
|
|
`INSERT INTO staff_pay_assignments
|
|
(id, tenant_id, employee_id, pay_rule_id, effective_from, params, pool_weight_bp, created_by)
|
|
VALUES ($1, $2, $3, $4, '2026-01-01', '{}'::jsonb, 0, $5)`,
|
|
[newUlid(), tenantA, newUlid(), newUlid(), actorB],
|
|
),
|
|
),
|
|
).rejects.toThrow();
|
|
|
|
await expect(
|
|
database.tenantTransaction(tenantB, actorB, (client) =>
|
|
client.query(
|
|
`INSERT INTO staff_payroll_ledger
|
|
(id, tenant_id, employee_id, amount_minor, currency_code, business_date, run_id, created_by)
|
|
VALUES ($1, $2, $3, 1000, 'RUB', '2026-01-01', $4, $5)`,
|
|
[newUlid(), tenantA, newUlid(), newUlid(), actorB],
|
|
),
|
|
),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|