test(tenant): fix payroll ledger isolation query and verify migration rls
This commit is contained in:
parent
37da6f0418
commit
96a28f575f
3 changed files with 64 additions and 10 deletions
|
|
@ -47,13 +47,6 @@ describeDatabase('PostgreSQL tenant isolation', () => {
|
|||
);
|
||||
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(
|
||||
|
|
@ -80,9 +73,9 @@ describeDatabase('PostgreSQL tenant isolation', () => {
|
|||
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],
|
||||
(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();
|
||||
|
|
|
|||
|
|
@ -619,4 +619,14 @@ describe('Tenant and employee isolation guarantees', () => {
|
|||
|
||||
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;',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
expect(sql).toContain('CREATE TABLE staff_payroll_ledger');
|
||||
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)");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue