test(tenant): fix payroll ledger isolation query and verify migration rls

This commit is contained in:
Ochenstarik 2026-09-24 19:11:45 +07:00
parent 37da6f0418
commit 96a28f575f
3 changed files with 64 additions and 10 deletions

View file

@ -47,13 +47,6 @@ describeDatabase('PostgreSQL tenant isolation', () => {
); );
expect(visibleToB.rowCount).toBe(0); 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( await expect(
database.tenantTransaction(tenantB, actorB, (client) => database.tenantTransaction(tenantB, actorB, (client) =>
client.query( client.query(
@ -80,9 +73,9 @@ describeDatabase('PostgreSQL tenant isolation', () => {
database.tenantTransaction(tenantB, actorB, (client) => database.tenantTransaction(tenantB, actorB, (client) =>
client.query( client.query(
`INSERT INTO staff_payroll_ledger `INSERT INTO staff_payroll_ledger
(id, tenant_id, employee_id, amount_minor, currency_code, business_date, run_id, created_by) (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, 1000, 'RUB', '2026-01-01', $4, $5)`, VALUES ($1, $2, $3, '2026-01-01', 'accrual', 1000, 'RUB', 'payroll_run', $4, $5, 'test', $6, $7)`,
[newUlid(), tenantA, newUlid(), newUlid(), actorB], [newUlid(), tenantA, newUlid(), newUlid(), newUlid(), actorB, newUlid()],
), ),
), ),
).rejects.toThrow(); ).rejects.toThrow();

View file

@ -619,4 +619,14 @@ describe('Tenant and employee isolation guarantees', () => {
expect(operationsSrc).toContain('!canManage && emp.core_user_id !== context.actorId && emp.id !== context.actorId'); expect(operationsSrc).toContain('!canManage && emp.core_user_id !== context.actorId && emp.id !== context.actorId');
}); });
it('confirms external_key and unique index are declared in 0008_employee_access migration', () => {
const migrationSql = readFileSync('migrations/0008_employee_access.up.sql', 'utf8');
expect(migrationSql).toContain('ALTER TABLE staff_payroll_ledger ADD COLUMN external_key TEXT;');
expect(migrationSql).toContain('CREATE UNIQUE INDEX staff_payroll_ledger_external_key_idx');
expect(migrationSql).toContain(
'ON staff_payroll_ledger (tenant_id, external_key) WHERE external_key IS NOT NULL;',
);
}); });
});

View file

@ -78,6 +78,32 @@ describe('payroll foundation migration', () => {
); );
}); });
const payrollTables = [
'staff_pay_rules',
'staff_pay_assignments',
'staff_payroll_runs',
'staff_payroll_lines',
'staff_payroll_ledger',
];
it.each(payrollTables)('verifies %s has tenant_id column', (table) => {
const create = sql.slice(
sql.indexOf(`CREATE TABLE ${table}`),
sql.indexOf(';', sql.indexOf(`CREATE TABLE ${table}`)),
);
expect(create).toContain('tenant_id VARCHAR(26) NOT NULL');
});
it('enforces tenant isolation and RLS on staff_payroll_ledger with select and insert policies', () => {
expect(sql).toContain('ALTER TABLE staff_payroll_ledger ENABLE ROW LEVEL SECURITY');
expect(sql).toContain('ALTER TABLE staff_payroll_ledger FORCE ROW LEVEL SECURITY');
expect(sql).toContain('CREATE POLICY staff_payroll_ledger_tenant_select ON staff_payroll_ledger');
expect(sql).toContain("FOR SELECT USING (tenant_id = current_setting('app.tenant_id', true))");
expect(sql).toContain('CREATE POLICY staff_payroll_ledger_tenant_insert ON staff_payroll_ledger');
expect(sql).toContain("tenant_id = current_setting('app.tenant_id', true)");
expect(sql).toContain("created_by = current_setting('app.user_id', true)");
});
it('keeps the money ledger append-only for the runtime role', () => { it('keeps the money ledger append-only for the runtime role', () => {
expect(sql).toContain('CREATE TABLE staff_payroll_ledger'); expect(sql).toContain('CREATE TABLE staff_payroll_ledger');
expect(sql).toContain('ALTER TABLE staff_payroll_ledger ENABLE ROW LEVEL SECURITY'); expect(sql).toContain('ALTER TABLE staff_payroll_ledger ENABLE ROW LEVEL SECURITY');
@ -88,3 +114,28 @@ describe('payroll foundation migration', () => {
); );
}); });
}); });
describe('employee access migration', () => {
const sql = readFileSync('migrations/0008_employee_access.up.sql', 'utf8');
it('declares external_key and staff_payroll_ledger_external_key_idx index on staff_payroll_ledger', () => {
expect(sql).toContain('ALTER TABLE staff_payroll_ledger ADD COLUMN external_key TEXT;');
expect(sql).toContain('CREATE UNIQUE INDEX staff_payroll_ledger_external_key_idx');
expect(sql).toContain(
'ON staff_payroll_ledger (tenant_id, external_key) WHERE external_key IS NOT NULL;',
);
});
it('protects standalone employee access with tenant isolation and RLS', () => {
const create = sql.slice(
sql.indexOf('CREATE TABLE staff_standalone_employee_access'),
sql.indexOf(';', sql.indexOf('CREATE TABLE staff_standalone_employee_access')),
);
expect(create).toContain('tenant_id VARCHAR(26) NOT NULL');
expect(sql).toContain('ALTER TABLE staff_standalone_employee_access ENABLE ROW LEVEL SECURITY');
expect(sql).toContain('ALTER TABLE staff_standalone_employee_access FORCE ROW LEVEL SECURITY');
expect(sql).toContain('CREATE POLICY staff_standalone_employee_access_tenant_isolation');
expect(sql).toContain("tenant_id = current_setting('app.tenant_id', true)");
});
});