187 lines
7.2 KiB
JavaScript
187 lines
7.2 KiB
JavaScript
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</option>') < response.body.indexOf('>announcements</option>'));
|
|
assert.ok(response.body.indexOf('>announcements</option>') < response.body.indexOf('>api-sources</option>'));
|
|
assert.ok(response.body.indexOf('>api-sources</option>') < response.body.indexOf('>canvas-sizes</option>'));
|
|
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 Windows</);
|
|
assert.doesNotMatch(response.body, /Mozilla\/5\.0 \(Windows NT 10\.0; Win64; x64\)/);
|
|
assert.match(response.body, /content\.body\.color/);
|
|
assert.match(response.body, />red</);
|
|
assert.match(response.body, />blue</);
|
|
assert.doesNotMatch(response.body, /content\.body\.keep/);
|
|
assert.match(response.body, /permissions removed/);
|
|
assert.match(response.body, /permissions added/);
|
|
assert.match(response.body, /dashboard\.read/);
|
|
assert.match(response.body, /playlists\.read/);
|
|
assert.match(response.body, /backgroundColor added/);
|
|
assert.match(response.body, /logoPath added/);
|
|
assert.ok(response.body.indexOf('login.success') < response.body.indexOf('login.failed'));
|
|
assert.match(response.body, />Export</);
|
|
assert.ok(handlers['/settings/audit-log/export']);
|
|
const pageQuery = queries.find(([sql]) => 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/);
|
|
}); |