Some checks failed
Deploy to Staging / Build Images (pull_request) Successful in 4m42s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 37s
Deploy to Staging / Verify Staging (pull_request) Failing after 6s
Deploy to Staging / Notify Staging Ready (pull_request) Has been skipped
Deploy to Staging / Notify Staging Failure (pull_request) Successful in 6s
- Add audit_logs table with categories, severities, and indexes - Create AuditLogService and AuditLogRepository - Add REST API endpoints for viewing and exporting logs - Wire audit logging into auth, vehicles, admin, and backup features - Add desktop AdminLogsPage with filters and CSV export - Add mobile AdminLogsMobileScreen with card layout - Implement 90-day retention cleanup job - Remove old AuditLogPanel from AdminCatalogPage Security fixes: - Escape LIKE special characters to prevent pattern injection - Limit CSV export to 5000 records to prevent memory exhaustion - Add truncation warning headers for large exports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
4.0 KiB
TypeScript
131 lines
4.0 KiB
TypeScript
/**
|
|
* @ai-summary Integration tests for audit_logs table migration
|
|
* @ai-context Tests table creation, constraints, and indexes
|
|
*/
|
|
|
|
import { Pool } from 'pg';
|
|
import { appConfig } from '../../../core/config/config-loader';
|
|
|
|
describe('Audit Logs Migration', () => {
|
|
let pool: Pool;
|
|
|
|
beforeAll(async () => {
|
|
pool = new Pool({
|
|
connectionString: appConfig.getDatabaseUrl(),
|
|
});
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await pool.end();
|
|
});
|
|
|
|
describe('Table Structure', () => {
|
|
it('should have audit_logs table with correct columns', async () => {
|
|
const result = await pool.query(`
|
|
SELECT column_name, data_type, is_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_name = 'audit_logs'
|
|
ORDER BY ordinal_position
|
|
`);
|
|
|
|
const columns = result.rows.map((row) => row.column_name);
|
|
expect(columns).toContain('id');
|
|
expect(columns).toContain('category');
|
|
expect(columns).toContain('severity');
|
|
expect(columns).toContain('user_id');
|
|
expect(columns).toContain('action');
|
|
expect(columns).toContain('resource_type');
|
|
expect(columns).toContain('resource_id');
|
|
expect(columns).toContain('details');
|
|
expect(columns).toContain('created_at');
|
|
});
|
|
});
|
|
|
|
describe('CHECK Constraints', () => {
|
|
it('should accept valid category values', async () => {
|
|
const validCategories = ['auth', 'vehicle', 'user', 'system', 'admin'];
|
|
|
|
for (const category of validCategories) {
|
|
const result = await pool.query(
|
|
`INSERT INTO audit_logs (category, severity, action)
|
|
VALUES ($1, 'info', 'test action')
|
|
RETURNING id`,
|
|
[category]
|
|
);
|
|
expect(result.rows[0].id).toBeDefined();
|
|
|
|
// Cleanup
|
|
await pool.query('DELETE FROM audit_logs WHERE id = $1', [result.rows[0].id]);
|
|
}
|
|
});
|
|
|
|
it('should reject invalid category values', async () => {
|
|
await expect(
|
|
pool.query(
|
|
`INSERT INTO audit_logs (category, severity, action)
|
|
VALUES ('invalid', 'info', 'test action')`
|
|
)
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it('should accept valid severity values', async () => {
|
|
const validSeverities = ['info', 'warning', 'error'];
|
|
|
|
for (const severity of validSeverities) {
|
|
const result = await pool.query(
|
|
`INSERT INTO audit_logs (category, severity, action)
|
|
VALUES ('auth', $1, 'test action')
|
|
RETURNING id`,
|
|
[severity]
|
|
);
|
|
expect(result.rows[0].id).toBeDefined();
|
|
|
|
// Cleanup
|
|
await pool.query('DELETE FROM audit_logs WHERE id = $1', [result.rows[0].id]);
|
|
}
|
|
});
|
|
|
|
it('should reject invalid severity values', async () => {
|
|
await expect(
|
|
pool.query(
|
|
`INSERT INTO audit_logs (category, severity, action)
|
|
VALUES ('auth', 'invalid', 'test action')`
|
|
)
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('Nullable Columns', () => {
|
|
it('should allow NULL user_id for system actions', async () => {
|
|
const result = await pool.query(
|
|
`INSERT INTO audit_logs (category, severity, user_id, action)
|
|
VALUES ('system', 'info', NULL, 'system startup')
|
|
RETURNING id, user_id`
|
|
);
|
|
|
|
expect(result.rows[0].id).toBeDefined();
|
|
expect(result.rows[0].user_id).toBeNull();
|
|
|
|
// Cleanup
|
|
await pool.query('DELETE FROM audit_logs WHERE id = $1', [result.rows[0].id]);
|
|
});
|
|
});
|
|
|
|
describe('Indexes', () => {
|
|
it('should have required indexes', async () => {
|
|
const result = await pool.query(`
|
|
SELECT indexname
|
|
FROM pg_indexes
|
|
WHERE tablename = 'audit_logs'
|
|
`);
|
|
|
|
const indexNames = result.rows.map((row) => row.indexname);
|
|
expect(indexNames).toContain('idx_audit_logs_category_created');
|
|
expect(indexNames).toContain('idx_audit_logs_severity_created');
|
|
expect(indexNames).toContain('idx_audit_logs_user_created');
|
|
expect(indexNames).toContain('idx_audit_logs_created');
|
|
expect(indexNames).toContain('idx_audit_logs_action_gin');
|
|
});
|
|
});
|
|
});
|