Release 2.16.2
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m13s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 31s

This commit is contained in:
lzstealth committed 2026-10-21 01:38:22 +01:00
1 parent 8ce8228091
commit 49df51d92f
32 files changed
+390 -172

No files matched your search

+132 -6
View File
@@ -1,6 +1,6 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { buildApiGroupPayload, saveApiGroup } = require('../src/data/api-source-groups');
const { buildApiGroupPayload, saveApiGroup, fetchApiGroupById } = require('../src/data/api-source-groups');
const { getPendingMigrations } = require('../src/db/migrations');
const { fetchApiSourcesData, fetchApiSourceById, testApiSourceAuthentication } = require('../src/data/api-sources');
@@ -8,7 +8,7 @@ function payload(requests, auth) {
return buildApiGroupPayload(Object.assign({ name: 'Tiltify', auth_method: 'none', requests_json: JSON.stringify(requests) }, auth));
}
function createPool(existing, slides) {
function createPool(existing, slides, groupEnabled = 1) {
const calls = [];
const connection = {
async beginTransaction() { calls.push(['begin']); },
@@ -17,7 +17,7 @@ function createPool(existing, slides) {
release() { calls.push(['release']); },
async query(sql, params) {
calls.push([sql, params]);
if (sql.includes('SELECT id FROM i_api_source_groups WHERE id')) return [[{ id: 5 }]];
if (sql.includes('SELECT id, enabled FROM i_api_source_groups WHERE id')) return [[{ id: 5, enabled: groupEnabled }]];
if (sql.includes('SELECT id FROM i_api_source_groups WHERE name')) return [[]];
if (sql.includes('SELECT id FROM i_api_sources')) return [existing.map(id => ({ id }))];
if (sql.includes('SELECT content_json')) return [(slides || []).map(value => ({ content_json: value }))];
@@ -81,14 +81,29 @@ test('2.16.1 migration preserves source IDs and backfills parent authentication'
return [[]];
} };
const pending = await getPendingMigrations(pool, { currentVersion: '2.16.0' });
assert.equal(pending.length, 1);
assert.equal(pending[0].version, '2.16.1');
await pending[0].run(pool);
const migration = pending.find(value => value.version === '2.16.1');
assert.ok(migration);
await migration.run(pool);
assert.ok(calls.some(([sql]) => sql.includes('INSERT IGNORE INTO i_api_source_groups (id,') && sql.includes('SELECT id,')));
assert.ok(calls.some(([sql]) => sql.includes('SET auth_source_id = id WHERE auth_source_id IS NULL')));
assert.ok(!calls.some(([sql]) => /UPDATE i_api_sources SET id\s*=/.test(sql)));
});
test('2.16.2 adds an enabled master switch without rewriting individual requests', async () => {
const calls = [];
const pool = { async query(sql) {
calls.push(sql);
if (sql.includes('information_schema.COLUMNS')) return [[{ column_count: 0 }]];
return [[]];
} };
const pending = await getPendingMigrations(pool, { currentVersion: '2.16.1' });
assert.equal(pending.length, 1);
assert.equal(pending[0].version, '2.16.2');
await pending[0].run(pool);
assert.ok(calls.some(sql => sql.includes('ALTER TABLE i_api_source_groups ADD COLUMN enabled TINYINT(1) NOT NULL DEFAULT 1')));
assert.ok(!calls.some(sql => sql.includes('UPDATE i_api_sources')));
});
test('editor/player API lists exclude credentials while refresh lookup resolves group authentication', async () => {
const queries = [];
const pool = { async query(sql) { queries.push(sql); return [[]]; } };
@@ -116,4 +131,115 @@ test('authentication-only tests never call a data endpoint', async () => {
assert.equal(publicAuth.ok, true);
assert.equal(calls.length, 1);
} finally { global.fetch = originalFetch; }
});
test('API refresh queues a saved request without redirects and scopes task status to that request', async () => {
const registerRoutes = require('../src/web/routes/data-sources/api-sources/routes');
const routes = { get: {}, post: {} };
const queued = [];
let enabled = 1;
let taskKey = 'api-source-refresh:43';
const pool = { async query(sql, params) {
if (sql.startsWith('SELECT * FROM i_api_source_groups')) return [[{ id: 5, name: 'Tiltify' }]];
if (sql.includes('WHERE request.auth_source_id')) return [[{ id: 43, request_name: 'Event', enabled }]];
if (sql.startsWith('SELECT id FROM i_api_sources WHERE id')) return [params[0] === 43 && params[1] === 5 ? [{ id: 43 }] : []];
throw new Error('Unexpected query: ' + sql);
} };
registerRoutes({ get(path, ...handlers) { routes.get[path] = handlers; }, post(path, ...handlers) { routes.post[path] = handlers; } }, {
pool, common: {}, requirePermission: permission => permission, getAuditUserId: () => 1,
backgroundTaskQueue: { async enqueueTask(task) { queued.push(task); return { id: 101 }; } },
dataSourceTasks: { async getTaskStatusById() { return { id: 101, key: taskKey, status: 'completed' }; } }
});
function response() {
return { statusCode: 200, headers: {}, status(code) { this.statusCode = code; return this; }, set(key, value) { this.headers[key] = value; return this; }, json(body) { this.body = body; return this; }, redirect() { throw new Error('AJAX refresh must not redirect'); } };
}
const refresh = routes.post['/data-sources/api-sources/:id/requests/:requestId/refresh'];
const state = routes.get['/data-sources/api-sources/:id/requests/:requestId/state'];
assert.equal(refresh[0], 'api-sources.allow');
assert.equal(state[0], 'api-sources.allow');
const req = { params: { id: '5', requestId: '43' }, get: () => 'application/json', query: { refresh_task_id: '101' } };
const next = error => { throw error; };
const started = response();
await refresh[1](req, started, next);
assert.equal(started.statusCode, 202);
assert.equal(started.body.taskId, 101);
assert.equal(started.body.stateUrl, '/data-sources/api-sources/5/requests/43/state');
assert.equal(queued[0].payload.sourceId, 43);
const completed = response();
await state[1](req, completed, next);
assert.equal(completed.body.status, 'completed');
assert.equal(completed.headers['Cache-Control'], 'no-store');
taskKey = 'api-source-refresh:44';
const wrongTask = response();
await state[1](req, wrongTask, next);
assert.equal(wrongTask.statusCode, 404);
const wrongGroup = response();
await state[1]({ ...req, params: { id: '6', requestId: '43' } }, wrongGroup, next);
assert.equal(wrongGroup.statusCode, 404);
enabled = 0;
const disabled = response();
await refresh[1](req, disabled, next);
assert.equal(disabled.statusCode, 409);
assert.equal(queued.length, 1);
});
test('saving a disabled group does not re-enable it or erase individual request switches', async () => {
const pool = createPool([5, 6], [], 0);
const result = await saveApiGroup(pool, 5, payload([
{ id: 5, name: 'Event', api_url: 'https://example.com/event', enabled: true },
{ id: 6, name: 'Donors', api_url: 'https://example.com/donors', enabled: false }
]), 1, true);
assert.equal(result.enabled, 0);
assert.deepEqual(result.requests.map(request => request.enabled), [1, 0]);
const parentUpdate = pool.calls.find(([sql]) => sql.startsWith('UPDATE i_api_source_groups'));
assert.doesNotMatch(parentUpdate[0], /\benabled\s*=/);
});
test('disabled parent groups preserve raw request switches in the editor', async () => {
const pool = { async query(sql) {
if (sql.startsWith('SELECT * FROM i_api_source_groups')) return [[{ id: 5, enabled: 0 }]];
return [[{ id: 43, request_name: 'Event', enabled: 0, request_enabled: 1, source_enabled: 0 }]];
} };
const group = await fetchApiGroupById(pool, 5);
assert.equal(group.enabled, 0);
assert.equal(group.requests[0].enabled, 1);
assert.equal(group.requests[0].source_enabled, 0);
});
test('the master toggle changes only the group and resumes only individually enabled requests', async () => {
const registerRoutes = require('../src/web/routes/data-sources/api-sources/routes');
const routes = {};
const queries = [];
const removed = [];
const registered = [];
const requests = [{ id: 43, request_name: 'Event', request_enabled: 1 }, { id: 44, request_name: 'Donors', request_enabled: 0 }];
let enabled = 1;
const pool = { async query(sql, params) {
queries.push(sql);
if (sql.startsWith('SELECT * FROM i_api_source_groups')) return [[{ id: 5, name: 'Tiltify', enabled }]];
if (sql.includes('WHERE request.auth_source_id')) return [requests.map(request => ({ ...request, enabled: enabled && request.request_enabled, source_enabled: enabled, update_interval_value: 30, update_interval_unit: 'seconds' }))];
if (sql.startsWith('UPDATE i_api_source_groups SET enabled')) { enabled = params[0]; return [{}]; }
throw new Error('Unexpected query: ' + sql);
} };
registerRoutes({ get() {}, post(path, ...handlers) { routes[path] = handlers; } }, {
pool, common: {}, requirePermission: permission => permission, getAuditUserId: () => 1,
dataSourceTasks: { removeRecurringRefresh(type, id) { removed.push(id); }, registerRecurringRefresh(type, id) { registered.push(id); } }
});
const res = { redirect(url) { this.url = url; }, status(code) { this.statusCode = code; return this; }, json(body) { this.body = body; } };
const next = error => { throw error; };
const route = routes['/data-sources/api-sources/:id'];
assert.equal(route[0], 'api-sources.update');
const req = { params: { id: '5' }, body: { data_source_action: 'toggle', name: 'Unsaved draft must not be saved' } };
await route[1](req, res, next);
assert.equal(enabled, 0);
assert.deepEqual(removed, [43, 44]);
assert.deepEqual(registered, []);
assert.deepEqual(requests.map(request => request.request_enabled), [1, 0]);
await routes['/data-sources/api-sources/:id/requests/:requestId/refresh'][1]({ params: { id: '5', requestId: '43' } }, res, next);
assert.equal(res.statusCode, 409);
assert.match(res.body.error, /source is disabled/);
await route[1](req, res, next);
assert.equal(enabled, 1);
assert.deepEqual(registered, [43]);
assert.ok(!queries.some(sql => sql.startsWith('UPDATE i_api_sources')));
});