Release v2.17.5
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m11s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-23 21:23:43 +01:00
1 parent 46636939de
commit 86b9609ea7
11 files changed
+285 -56

No files matched your search

+72 -2
View File
@@ -7,6 +7,7 @@ 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;
@@ -15,8 +16,16 @@ test('audit log uses its own read permission and shared pagination partial', asy
registerAuditLogRoutes(app, {
pool: {
async query(sql) {
if (String(sql).includes('FROM o_audit_events')) {
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,
@@ -102,6 +111,12 @@ test('audit log uses its own read permission and shared pagination partial', asy
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: {},
@@ -114,4 +129,59 @@ test('audit log uses its own read permission and shared pagination partial', asy
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/);
});
+31
View File
@@ -49,6 +49,37 @@ test('pending migrations are empty when the schema already matches the app versi
assert.equal(pendingMigrations.length, 0);
});
test('query index migration upgrades a 2.17.4 installation', async () => {
const queries = [];
const pool = {
queries,
async query(sql, params) {
queries.push([sql, params]);
if (String(sql).includes('FROM information_schema.STATISTICS')) {
return [[{ index_count: params[1] === 'idx_background_tasks_status' ? 1 : 0 }]];
}
return [[]];
}
};
const pending = await getPendingMigrations(pool, { currentVersion: '2.17.4' });
const indexMigration = pending.find(migration => migration.version === '2.17.5');
assert.ok(indexMigration);
await indexMigration.run(pool);
const alterations = queries.map(([sql]) => String(sql)).filter(sql => sql.startsWith('ALTER TABLE'));
assert.equal(alterations.length, 9);
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_playlist_slides_playlist_position')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_template_regions_template_z')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_slides_template')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_templates_canvas_size')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_sessions_expires_at')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_background_tasks_status_created_at')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_rss_feeds_modified_at')));
assert.ok(alterations.some(sql => sql.includes('ADD INDEX idx_api_source_groups_modified_at')));
assert.ok(alterations.some(sql => sql.includes('DROP INDEX idx_background_tasks_status')));
});
test('playlist shuffle migration upgrades an existing 2.15.0 installation', async () => {
const pool = createPool([]);
const pending = await getPendingMigrations(pool, { currentVersion: '2.15.0' });
+6 -4
View File
@@ -161,8 +161,9 @@ test('2.17.0 migration adds the expiring view-as session columns', async () => {
]);
const pending = await getPendingMigrations(pool, { currentVersion: '2.16.2' });
assert.deepEqual(pending.map(function (migration) { return migration.version; }), ['2.17.0', '2.17.1']);
await pending[0].run(pool);
const viewAsMigration = pending.find(function (migration) { return migration.version === '2.17.0'; });
assert.ok(viewAsMigration);
await viewAsMigration.run(pool);
assert.ok(pool.queries.some(function ([sql]) {
return String(sql).includes('ALTER TABLE a_sessions ADD COLUMN view_as_user_id INT NULL AFTER user_id');
@@ -189,8 +190,9 @@ test('2.17.1 migration removes obsolete dashboard.allow grants and permission',
]);
const pending = await getPendingMigrations(pool, { currentVersion: '2.17.0' });
assert.deepEqual(pending.map(function (migration) { return migration.version; }), ['2.17.1']);
await pending[0].run(pool);
const obsoletePermissionMigration = pending.find(function (migration) { return migration.version === '2.17.1'; });
assert.ok(obsoletePermissionMigration);
await obsoletePermissionMigration.run(pool);
assert.ok(pool.queries.some(function ([sql, params]) {
return String(sql).includes('DELETE role_permissions') && params[0] === 'dashboard.allow';