141 lines
6.3 KiB
TypeScript
141 lines
6.3 KiB
TypeScript
import { readFileSync } from 'node:fs';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
describe('tenant isolation migration', () => {
|
|
const sql = readFileSync('migrations/0001_infrastructure.up.sql', 'utf8');
|
|
const tables = ['staff_outbox', 'staff_inbox', 'staff_idempotency_keys', 'staff_audit_log'];
|
|
it.each(tables)('%s is tenant scoped and protected by RLS', (table) => {
|
|
const create = sql.slice(
|
|
sql.indexOf(`CREATE TABLE IF NOT EXISTS ${table}`),
|
|
sql.indexOf(';', sql.indexOf(`CREATE TABLE IF NOT EXISTS ${table}`)),
|
|
);
|
|
expect(create).toContain('tenant_id VARCHAR(26) NOT NULL');
|
|
expect(sql).toContain(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
|
|
expect(sql).toContain(`ALTER TABLE ${table} FORCE ROW LEVEL SECURITY`);
|
|
expect(sql).toMatch(new RegExp(`CREATE POLICY [a-z_]+ ON ${table} `));
|
|
});
|
|
it('sets tenant and actor locally inside each transaction', () => {
|
|
const source = readFileSync('src/infrastructure/database.ts', 'utf8');
|
|
expect(source).toContain("set_config('app.tenant_id', $1, true)");
|
|
expect(source).toContain("set_config('app.user_id', $2, true)");
|
|
});
|
|
});
|
|
|
|
describe('attendance portal migrations', () => {
|
|
const sql = readFileSync('migrations/0005_attendance_portal.up.sql', 'utf8');
|
|
const tables = ['staff_employees', 'staff_attendance_events', 'staff_timesheet_entries'];
|
|
|
|
it.each(tables)('%s is tenant scoped and forces RLS', (table) => {
|
|
expect(sql).toContain(`ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY`);
|
|
expect(sql).toContain(`ALTER TABLE ${table} FORCE ROW LEVEL SECURITY`);
|
|
expect(sql).toMatch(new RegExp(`CREATE POLICY ${table}_[a-z_]+ ON ${table}`));
|
|
});
|
|
|
|
it('keeps raw attendance immutable for the runtime role', () => {
|
|
expect(sql).toContain('GRANT SELECT, INSERT ON staff_attendance_events TO krona_staff_app');
|
|
expect(sql).toContain(
|
|
'REVOKE UPDATE, DELETE, TRUNCATE ON staff_attendance_events FROM krona_staff_app',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('payroll foundation migration', () => {
|
|
const sql = readFileSync('migrations/0006_payroll_foundation.up.sql', 'utf8');
|
|
const tables = [
|
|
'staff_org_units',
|
|
'staff_org_unit_terms',
|
|
'staff_metric_definitions',
|
|
'staff_daily_metrics',
|
|
'staff_work_items',
|
|
'staff_pay_rules',
|
|
'staff_pay_assignments',
|
|
'staff_work_sessions',
|
|
'staff_payroll_runs',
|
|
'staff_payroll_lines',
|
|
'staff_payouts',
|
|
];
|
|
|
|
it.each(tables)('%s is tenant scoped and included in the RLS setup', (table) => {
|
|
expect(sql).toContain(`CREATE TABLE ${table}`);
|
|
expect(sql).toContain(`'${table}'`);
|
|
});
|
|
|
|
it('enables and forces RLS with a tenant policy for every listed payroll table', () => {
|
|
expect(sql).toContain("EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', target_table)");
|
|
expect(sql).toContain("EXECUTE format('ALTER TABLE %I FORCE ROW LEVEL SECURITY', target_table)");
|
|
expect(sql).toContain("target_table || '_tenant_isolation', target_table");
|
|
});
|
|
|
|
it('uses tenant-scoped foreign keys for payroll ownership', () => {
|
|
expect(sql).toContain(
|
|
'FOREIGN KEY (tenant_id, employee_id) REFERENCES staff_employees (tenant_id, id)',
|
|
);
|
|
expect(sql).toContain(
|
|
'FOREIGN KEY (tenant_id, org_unit_id) REFERENCES staff_org_units (tenant_id, id)',
|
|
);
|
|
expect(sql).toContain(
|
|
'FOREIGN KEY (tenant_id, pay_rule_id) REFERENCES staff_pay_rules (tenant_id, id)',
|
|
);
|
|
});
|
|
|
|
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');
|
|
expect(sql).toContain('ALTER TABLE staff_payroll_ledger FORCE ROW LEVEL SECURITY');
|
|
expect(sql).toContain('GRANT SELECT, INSERT ON staff_payroll_ledger TO krona_staff_app');
|
|
expect(sql).toContain(
|
|
'REVOKE UPDATE, DELETE, TRUNCATE ON staff_payroll_ledger FROM krona_staff_app',
|
|
);
|
|
});
|
|
});
|
|
|
|
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)");
|
|
});
|
|
});
|
|
|