Release v2.20.1: migrate authentication to Better Auth
This commit is contained in:
1 parent
e9a53f577c
commit
fba4f99ff7
92 files changed
+5085
-1943
No files matched your search
@@ -3,7 +3,7 @@ const assert = require('node:assert/strict');
|
||||
|
||||
require('../src/common');
|
||||
|
||||
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, buildAuditChanges, recordRequestAuditEvent } = require('../src/data/audit-log');
|
||||
const { AUDIT_CATEGORY_KEYS, AUDIT_CATEGORY_LABELS, buildAuditChanges, buildSlideAuditChanges, recordRequestAuditEvent } = require('../src/data/audit-log');
|
||||
const { getDefaultAppSettings } = require('../src/data/app-settings');
|
||||
|
||||
function createPool(settings) {
|
||||
@@ -68,6 +68,20 @@ test('audit writer omits request metadata when disabled', async () => {
|
||||
assert.equal(pool.inserts[0][7], null);
|
||||
});
|
||||
|
||||
test('audit writer skips empty update diffs for content but preserves action events and other details', async () => {
|
||||
const categories = ['slides', 'templates', 'playlists', 'screens', 'canvas-sizes', 'weather'];
|
||||
const pool = createPool({ enabled: true, categories, includeMetadata: false });
|
||||
for (const category of categories) {
|
||||
await recordRequestAuditEvent(pool, request, { category, eventType: 'content.updated', details: { changes: {} } });
|
||||
}
|
||||
assert.equal(pool.inserts.length, 0);
|
||||
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.created' });
|
||||
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.deleted' });
|
||||
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.updated', details: { changes: {}, promotedSlideId: 7 } });
|
||||
await recordRequestAuditEvent(pool, request, { category: 'templates', eventType: 'template.updated', details: { changes: { 'regions.heading.x': { from: 1, to: 2 } } } });
|
||||
assert.equal(pool.inserts.length, 4);
|
||||
});
|
||||
|
||||
test('audit changes include only fields with different from and to values', () => {
|
||||
assert.deepEqual(buildAuditChanges({
|
||||
name: 'Old name',
|
||||
@@ -81,4 +95,126 @@ test('audit changes include only fields with different from and to values', () =
|
||||
name: { from: 'Old name', to: 'New name' },
|
||||
roleIds: { from: [1, 2], to: [2, 3] }
|
||||
});
|
||||
});
|
||||
|
||||
test('slide audit stores only changed leaf fields without unchanged content', () => {
|
||||
const previous = { title: 'Slide', content: { heading: { value: 'Old', font_size: 32 }, qr: { qr_svg: '<svg>' + 'x'.repeat(100000) + '</svg>' } } };
|
||||
const next = structuredClone(previous);
|
||||
next.content.heading.value = 'New';
|
||||
const changes = buildSlideAuditChanges(previous, next);
|
||||
assert.deepEqual(changes, { 'content.heading.value': { from: 'Old', to: 'New' } });
|
||||
assert.ok(JSON.stringify(changes).length < 100);
|
||||
});
|
||||
|
||||
test('slide audit ignores unchanged saves and object key order', () => {
|
||||
assert.deepEqual(buildSlideAuditChanges({ content: { body: { value: 'Same', size: 32 } } }, { content: { body: { size: 32, value: 'Same' } } }), {});
|
||||
});
|
||||
|
||||
test('slide audit preserves added and removed fields across JSON serialization', () => {
|
||||
const changes = buildSlideAuditChanges({ content: { removed: { value: 'Old' }, body: { muted: false } } }, { content: { added: { value: 'New' }, body: { muted: true } } });
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(changes)), {
|
||||
'content.removed.value': { from: 'Old', to: null },
|
||||
'content.body.muted': { from: false, to: true },
|
||||
'content.added.value': { from: null, to: 'New' }
|
||||
});
|
||||
});
|
||||
|
||||
test('slide audit fingerprints generated QR payloads including added and removed assets', () => {
|
||||
const previous = { content: { qr: { qr_svg: '<svg>' + 'x'.repeat(100000) + '</svg>', qr_preview: 'data:image/png;base64,' + 'y'.repeat(100000), value: 'https://example.com/old' } } };
|
||||
const next = { content: { qr: { qr_svg: '<svg>new</svg>', value: 'https://example.com/new' } } };
|
||||
const changes = buildSlideAuditChanges(previous, next);
|
||||
assert.match(changes['content.qr.qr_svg'].from, /^\[Generated asset: 100011 bytes; SHA-256 [a-f0-9]{64}\]$/);
|
||||
assert.notEqual(changes['content.qr.qr_svg'].from, changes['content.qr.qr_svg'].to);
|
||||
assert.equal(changes['content.qr.qr_preview'].to, null);
|
||||
assert.deepEqual(changes['content.qr.value'], { from: 'https://example.com/old', to: 'https://example.com/new' });
|
||||
assert.ok(JSON.stringify(changes).length < 1000);
|
||||
const added = buildSlideAuditChanges({}, next);
|
||||
assert.equal(added['content.qr.qr_svg'].from, null);
|
||||
assert.match(added['content.qr.qr_svg'].to, /SHA-256/);
|
||||
assert.equal(previous.content.qr.qr_svg.length, 100011);
|
||||
});
|
||||
|
||||
test('slide save skips no-op audit events while retaining save side effects', async () => {
|
||||
const registerContentRoutes = require('../src/web/routes/admin/content');
|
||||
for (const changed of [false, true]) {
|
||||
const handlers = {};
|
||||
const calls = [];
|
||||
const events = [];
|
||||
const content = { heading: { type: 'text', value: 'Old' } };
|
||||
const nextContent = { heading: { type: 'text', value: changed ? 'New' : 'Old' } };
|
||||
const noop = async () => {};
|
||||
const connection = {
|
||||
beginTransaction: noop,
|
||||
async commit() { calls.push('commit'); },
|
||||
rollback: noop,
|
||||
release() {},
|
||||
async query(sql) {
|
||||
if (sql.startsWith('SELECT id FROM c_slides')) return [[{ id: 7 }]];
|
||||
if (sql.startsWith('UPDATE c_slides')) calls.push('update');
|
||||
return [[]];
|
||||
}
|
||||
};
|
||||
registerContentRoutes({
|
||||
get() {},
|
||||
post(route, ...actions) { handlers[route] = actions; }
|
||||
}, {
|
||||
pool: { async query() { return [[]]; }, async getConnection() { return connection; } },
|
||||
common: {
|
||||
fetchSlideById: async () => ({ id: 7, title: 'Slide', template_id: 2, content }),
|
||||
buildSlidePayload: async () => ({ title: 'Slide', templateId: 2, contentJson: JSON.stringify(nextContent) }),
|
||||
fetchDuplicateName: async () => null,
|
||||
parseJsonSafe: JSON.parse
|
||||
},
|
||||
pages: {}, upload: { any: () => noop, single: () => noop },
|
||||
fetchScreensBySlideId: async () => [], fetchScreensByTemplateId: async () => [],
|
||||
collectUploadReferencesFromSlide: () => [], collectUploadReferencesFromTemplate: () => [],
|
||||
collectUploadReferencesFromPayload: () => [], removeUnusedUploadFiles: noop,
|
||||
syncPlaylistUploadsOnChange: async () => { calls.push('sync'); },
|
||||
getAuditUserId: () => 9,
|
||||
recordRequestAuditEvent: async (_pool, _req, event) => { events.push(event); },
|
||||
redirectAfterSave: () => { calls.push('redirect'); },
|
||||
notifyPlayerScreens: noop,
|
||||
broadcastDashboardState: async () => { calls.push('broadcast'); },
|
||||
backgroundTaskQueue: { enqueueTask: async () => { calls.push('thumbnail'); } },
|
||||
getSlideDeleteBlockMessage: noop, getTemplateDeleteBlockMessage: noop, getCanvasSizeDeleteBlockMessage: noop,
|
||||
hasAnyPermission: () => false, requirePermission: () => noop
|
||||
});
|
||||
await handlers['/slides/:id'].at(-1)({ params: { id: '7' }, files: [] }, {}, error => { throw error; });
|
||||
assert.deepEqual(calls, ['update', 'commit', 'sync', 'broadcast', 'thumbnail', 'redirect']);
|
||||
assert.equal(events.length, changed ? 1 : 0);
|
||||
if (changed) {
|
||||
assert.equal(events[0].eventType, 'slide.updated');
|
||||
assert.deepEqual(events[0].details.changes, { 'content.heading.value': { from: 'Old', to: 'New' } });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('shared audit changes compact nested content across categories', () => {
|
||||
assert.deepEqual(buildAuditChanges({ settings: { text: 'Old', keep: 'Same' } }, { settings: { text: 'New', keep: 'Same' } }), {
|
||||
'settings.text': { from: 'Old', to: 'New' }
|
||||
});
|
||||
});
|
||||
|
||||
test('template auditing matches regions by stable key and summarizes default QR assets', () => {
|
||||
const heading = { region_key: 'heading', x: 10, default_content: { value: 'Same' } };
|
||||
const qr = { region_key: 'qr', default_content: { qr_svg: '<svg>old</svg>' } };
|
||||
assert.deepEqual(buildAuditChanges({ regions: [heading, qr] }, { regions: [qr, heading] }), {});
|
||||
const changes = buildAuditChanges({ regions: [heading, qr] }, { regions: [qr, { ...heading, x: 20 }] });
|
||||
assert.deepEqual(changes, { 'regions.heading.x': { from: 10, to: 20 } });
|
||||
const assets = buildAuditChanges({ regions: [qr] }, { regions: [{ ...qr, default_content: { qr_svg: '<svg>new</svg>' } }] });
|
||||
assert.match(assets['regions.qr.default_content.qr_svg'].from, /SHA-256/);
|
||||
assert.match(assets['regions.qr.default_content.qr_svg'].to, /SHA-256/);
|
||||
});
|
||||
|
||||
test('ordered content collections retain reordering and added or removed item differences', () => {
|
||||
const first = { slideId: 1, duration: 10 };
|
||||
const second = { slideId: 2, duration: 10 };
|
||||
assert.deepEqual(buildAuditChanges({ items: [first, second] }, { items: [second, first] }), {
|
||||
'items[0].slideId': { from: 1, to: 2 },
|
||||
'items[1].slideId': { from: 2, to: 1 }
|
||||
});
|
||||
assert.deepEqual(buildAuditChanges({ items: [first, second] }, { items: [first] }), {
|
||||
'items[1].slideId': { from: 2, to: null },
|
||||
'items[1].duration': { from: 10, to: null }
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user