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(); const professionId = newUlid(); const employeeIdA = newUlid(); const subscriptionIdA = newUlid(); const deliveryIdA = 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_notification_deliveries WHERE id = $1', [deliveryIdA]); await client.query('DELETE FROM staff_push_subscriptions WHERE id = $1', [subscriptionIdA]); await client.query('DELETE FROM staff_outbox WHERE id = $1', [eventId]); await client.query('DELETE FROM staff_professions WHERE id = $1', [professionId]); await client.query('DELETE FROM staff_employees WHERE id = $1', [employeeIdA]); }); 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); await database.tenantTransaction(tenantA, actorA, (client) => client.query( `INSERT INTO staff_professions (id, tenant_id, code, name) VALUES ($1, $2, 'cook', 'Повар')`, [professionId, tenantA], ), ); const professionsVisibleToB = await database.tenantTransaction(tenantB, actorB, (client) => client.query('SELECT id FROM staff_professions WHERE id = $1', [professionId]), ); expect(professionsVisibleToB.rowCount).toBe(0); await expect( database.tenantTransaction(tenantB, actorB, (client) => client.query( `INSERT INTO staff_professions (id, tenant_id, code, name) VALUES ($1, $2, 'foreign', 'Чужая профессия')`, [newUlid(), tenantA], ), ), ).rejects.toThrow(); 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, business_date, kind, amount_minor, currency_code, ref_type, ref_id, correlation_id, note, created_by, external_key) VALUES ($1, $2, $3, '2026-01-01', 'accrual', 1000, 'RUB', 'payroll_run', $4, $5, 'test', $6, $7)`, [newUlid(), tenantA, newUlid(), newUlid(), newUlid(), actorB, newUlid()], ), ), ).rejects.toThrow(); // Push subscriptions tenant isolation await database.tenantTransaction(tenantA, actorA, async (client) => { await client.query( `INSERT INTO staff_employees (id, tenant_id, personnel_number, display_name) VALUES ($1, $2, 'EMP-PUSH-1', 'Тестовый сотрудник')`, [employeeIdA, tenantA], ); await client.query( `INSERT INTO staff_push_subscriptions (id, tenant_id, employee_id, endpoint, p256dh, auth) VALUES ($1, $2, $3, 'https://push.example.com/subA', 'test-p256dh', 'test-auth')`, [subscriptionIdA, tenantA, employeeIdA], ); }); const subVisibleToB = await database.tenantTransaction(tenantB, actorB, (client) => client.query('SELECT id FROM staff_push_subscriptions WHERE id = $1', [subscriptionIdA]), ); expect(subVisibleToB.rowCount).toBe(0); await expect( database.tenantTransaction(tenantB, actorB, (client) => client.query( `INSERT INTO staff_push_subscriptions (id, tenant_id, employee_id, endpoint, p256dh, auth) VALUES ($1, $2, $3, 'https://push.example.com/subB', 'key', 'auth')`, [newUlid(), tenantA, employeeIdA], ), ), ).rejects.toThrow(); // Notification deliveries tenant isolation await database.tenantTransaction(tenantA, actorA, async (client) => { await client.query( `INSERT INTO staff_notification_deliveries (id, tenant_id, employee_id, outbox_id, endpoint, status) VALUES ($1, $2, $3, $4, 'https://push.example.com/subA', 'delivered')`, [deliveryIdA, tenantA, employeeIdA, eventId], ); }); const delVisibleToB = await database.tenantTransaction(tenantB, actorB, (client) => client.query('SELECT id FROM staff_notification_deliveries WHERE id = $1', [deliveryIdA]), ); expect(delVisibleToB.rowCount).toBe(0); await expect( database.tenantTransaction(tenantB, actorB, (client) => client.query( `INSERT INTO staff_notification_deliveries (id, tenant_id, employee_id, outbox_id, endpoint, status) VALUES ($1, $2, $3, $4, 'https://push.example.com/subB', 'pending')`, [newUlid(), tenantA, employeeIdA, eventId], ), ), ).rejects.toThrow(); }); });