const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const registerAuditLogRoutes = require('../src/web/routes/settings/audit-log'); test('audit log uses its own read permission and shared pagination partial', async () => { const handlers = {}; const queries = []; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; registerAuditLogRoutes(app, { pool: { async query(sql, params) { const query = String(sql); queries.push([query, params || []]); if (query.includes('SELECT COUNT(*) AS count')) { return [[{ count: 2 }]]; } if (query.includes('SELECT DISTINCT e.event_type')) { return [[{ event_type: 'login.failed' }, { event_type: 'login.success' }]]; } if (query.includes('FROM o_audit_events')) { return [[ { id: 1, occurred_at: '2026-08-16T12:00:00.000Z', category: 'authentication', event_type: 'login.success', target_label: 'newer', user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36', details_json: JSON.stringify({ changes: { content: { from: { title: 'Same', body: { color: 'red', keep: 'same' } }, to: { title: 'Same', body: { color: 'blue', keep: 'same' } } }, permissions: { from: ['dashboard.read', 'screens.read'], to: ['screens.read', 'playlists.read'] }, backgroundColor: { from: null, to: '#111111' }, logoPath: { from: '', to: '/media/logo.png' } } }) }, { id: 2, occurred_at: '2026-08-15T12:00:00.000Z', category: 'authentication', event_type: 'login.failed', target_label: 'older', details_json: null } ]]; } return [[]]; } }, formatDashboardDate() { return 'Aug 16, 2026'; } }); assert.ok(handlers['/settings/audit-log']); const request = { currentUser: { id: 7, permissions: ['audit-log.read', 'audit-log.allow'] }, query: {} }; let continued = false; handlers['/settings/audit-log'][0](request, {}, function () { continued = true; }); assert.equal(continued, true); const response = { send(value) { this.body = value; } }; await handlers['/settings/audit-log'][1](request, response, function (error) { throw error; }); assert.match(response.body, /Audit log/); assert.ok(response.body.indexOf('>All categories') < response.body.indexOf('>announcements')); assert.ok(response.body.indexOf('>announcements') < response.body.indexOf('>api-sources')); assert.ok(response.body.indexOf('>api-sources') < response.body.indexOf('>canvas-sizes')); assert.match(response.body, /table-pagination/); assert.match(response.body, /data-local-datetime/); assert.match(response.body, /audit-event-type-options/); assert.match(response.body, /login\.success/); assert.match(response.body, />Chrome 150\.0\.0\.0 on WindowsredblueExport sql.includes('LIMIT ? OFFSET ?')); assert.ok(pageQuery); assert.match(pageQuery[0], /ORDER BY e\.occurred_at DESC, e\.id DESC LIMIT \? OFFSET \?/); assert.deepEqual(pageQuery[1].slice(-2), [25, 0]); assert.ok(queries.some(([sql]) => sql.includes('SELECT COUNT(*) AS count'))); assert.ok(queries.some(([sql]) => sql.includes('SELECT DISTINCT e.event_type') && /WHERE e\.event_type IS NOT NULL/.test(sql))); const exportResponse = { headers: {}, setHeader(name, value) { this.headers[name] = value; }, send(value) { this.body = value; } }; await handlers['/settings/audit-log/export'][1](request, exportResponse, function (error) { throw error; }); assert.match(exportResponse.headers['Content-Type'], /text\/csv/); assert.match(exportResponse.body, /Occurred At \(UTC\)/); assert.match(exportResponse.body, /login.success/); const exportQuery = queries.filter(([sql]) => sql.includes('FROM o_audit_events') && !sql.includes('SELECT COUNT(*)') && !sql.includes('SELECT DISTINCT')).at(-1); assert.ok(exportQuery); assert.doesNotMatch(exportQuery[0], /LIMIT \?/); }); test('audit log applies SQL filters, escaped search, and selected sort before pagination', async () => { const handlers = {}; const queries = []; const app = { get(path, ...routeHandlers) { handlers[path] = routeHandlers; } }; registerAuditLogRoutes(app, { pool: { async query(sql, params) { const query = String(sql); queries.push([query, params || []]); if (query.includes('SELECT COUNT(*) AS count')) return [[{ count: 40 }]]; return [[]]; } }, formatDashboardDate() { return ''; } }); await handlers['/settings/audit-log'][1]({ currentUser: { id: 7, permissions: ['audit-log.read'] }, query: { category: 'Authentication', event_type: 'Login.Success', search: ' Alice%_ ', sort: 'actor', direction: 'desc', page: '2' } }, { send() {} }, function (error) { throw error; }); const countQuery = queries.find(([sql]) => sql.includes('SELECT COUNT(*) AS count')); const pageQuery = queries.find(([sql]) => sql.includes('LIMIT ? OFFSET ?')); const eventTypesQuery = queries.find(([sql]) => sql.includes('SELECT DISTINCT e.event_type')); assert.ok(countQuery); assert.ok(pageQuery); assert.ok(eventTypesQuery); assert.match(countQuery[0], /e\.category = \? AND e\.event_type = \?/); assert.match(countQuery[0], /LOWER\(COALESCE\(CAST\(e\.category AS CHAR\)/); assert.deepEqual(countQuery[1].slice(0, 2), ['authentication', 'login.success']); assert.ok(countQuery[1].includes('%alice\\%\\_%')); assert.match(pageQuery[0], /ORDER BY COALESCE\(NULLIF\(u\.name, ''\), u\.username, 'System'\) DESC, e\.occurred_at DESC, e\.id DESC/); assert.deepEqual(pageQuery[1].slice(-2), [25, 25]); assert.match(eventTypesQuery[0], /AND e\.event_type IS NOT NULL/); });