const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const registerManageRoutes = require('../src/web/routes/admin/manage'); test('screen update keeps the existing slug on edit', async () => { const handlers = {}; const app = { post(path, ...routeHandlers) { handlers[path] = routeHandlers; }, get() {} }; const pool = { async query(sql) { if (sql.includes('SELECT id, name, slug, playlist_id')) { return [[{ id: 42, name: 'Old Screen', slug: 'alpha', playlist_id: null }]]; } if (sql.includes('UPDATE d_screens SET name = ?, slug = ?, playlist_id = ?, modified_by = ? WHERE id = ?')) { return [{ affectedRows: 1 }]; } throw new Error(`Unexpected SQL: ${sql}`); } }; const calls = []; const pages = { renderScreenFormPage() {}, renderScreenEditPage() {} }; const deps = { pool, common: { slugify(value) { return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); }, uniqueScreenSlug: async () => 'beta', fetchDuplicateName: async () => null, fetchScreenById: async () => ({ id: 42, name: 'Old Screen', slug: 'alpha', playlist_id: null }), fetchScreenPlayerRecord: async () => ({ public_base_url: 'http://player.local' }), fetchPlayerPublicBaseUrl: async () => 'http://player.local', fetchPlayerRegistrations: async () => ([ { public_base_url: 'http://player.local', internal_base_url: 'http://player.internal' } ]) }, pages, getAuditUserId() { return 7; }, redirectAfterSave(req, res, url) { calls.push({ kind: 'redirectAfterSave', url }); res.redirectedTo = url; }, notifyPlayerScreens: async (slugs, payload) => { calls.push({ kind: 'notifyPlayerScreens', slugs, payload }); return 1; }, broadcastDashboardState: async () => { calls.push({ kind: 'broadcastDashboardState' }); }, getScreenDeleteBlockMessage: async () => '', getScreenConnections: async (slug) => { if (slug === 'alpha') { return { connections: [ { id: 'alpha-1', clientId: 'alpha-1', deviceId: 'alpha-device', playerPublicBaseUrl: 'http://player.local/' } ] }; } return { connections: [] }; }, forwardPlayerCommand: async (slug, payload) => { calls.push({ kind: 'forwardPlayerCommand', slug, payload }); return { ok: true }; }, forwardPlayerCommandToBaseUrl: async (baseUrl, slug, payload) => { calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload }); return { ok: true }; }, playerPublicBaseUrl: 'http://player.example', requirePermission() { return function (_req, _res, next) { next(); }; } }; registerManageRoutes(app, deps); const routeHandlers = handlers['/screens/:id']; assert.equal(Array.isArray(routeHandlers), true); const req = { params: { id: '42' }, body: { name: 'Updated Screen', slug: 'beta' } }; const res = { redirect(url) { this.redirectedTo = url; }, status() { return this; }, send() { return this; } }; await routeHandlers[1](req, res, () => {}); assert.equal(res.redirectedTo, '/screens?edit=42'); assert.deepEqual(calls, [ { kind: 'redirectAfterSave', url: '/screens?edit=42' } ]); }); test('dashboard commands fan out to each connected bridge player', async () => { const handlers = {}; const app = { post(path, ...routeHandlers) { handlers[path] = routeHandlers; }, get() {} }; const calls = []; registerManageRoutes(app, { pool: { async query(sql) { if (sql.includes('SELECT s.slug') && sql.includes('FROM d_screens s') && sql.includes('ORDER BY s.slug ASC')) { return [[ { slug: 'alpha', }, { slug: 'beta' } ]]; } return [[]]; } }, common: { fetchPlayerRegistrations: async () => ([ { public_base_url: 'http://player-a.example', internal_base_url: 'http://player-a.internal' }, { public_base_url: 'http://player-b.example', internal_base_url: 'http://player-b.internal' } ]) }, pages: { renderScreenFormPage() {}, renderScreenEditPage() {} }, getAuditUserId() { return 7; }, redirectAfterSave() {}, notifyPlayerScreens: async () => 0, broadcastDashboardState: async () => { calls.push({ kind: 'broadcastDashboardState' }); }, getScreenDeleteBlockMessage: async () => '', getScreenConnections: async (slug) => { if (slug === 'alpha') { return { connections: [ { id: 'alpha-1', clientId: 'alpha-1', deviceId: 'alpha-device', playerPublicBaseUrl: 'http://player-a.example/' } ] }; } if (slug === 'beta') { return { connections: [ { id: 'beta-1', clientId: 'beta-1', deviceId: 'beta-device', playerPublicBaseUrl: 'http://player-b.example/' } ] }; } return { connections: [] }; }, forwardPlayerCommand: async (slug, payload) => { calls.push({ kind: 'forwardPlayerCommand', slug, payload }); return { ok: true }; }, forwardPlayerCommandToBaseUrl: async (baseUrl, slug, payload) => { calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload }); return { ok: true }; }, requirePermission() { return function (_req, _res, next) { next(); }; } }); const routeHandlers = handlers['/commands']; assert.equal(Array.isArray(routeHandlers), true); const req = { body: { command: 'pause' }, query: {} }; const res = { statusCode: 200, body: null, status(code) { this.statusCode = code; return this; }, json(value) { this.body = value; return this; } }; await routeHandlers[1](req, res, () => {}); assert.equal(res.statusCode, 200); assert.equal(res.body.ok, true); assert.equal(res.body.sent, 2); assert.deepEqual( calls.filter((entry) => entry.kind === 'forwardPlayerCommandToBaseUrl').map((entry) => entry.baseUrl), ['http://player-a.internal', 'http://player-b.internal'] ); assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand'), false); assert.equal(calls.some((entry) => entry.kind === 'broadcastDashboardState'), true); }); test('dashboard commands use live bridge connections when available', async () => { const handlers = {}; const app = { post(path, ...routeHandlers) { handlers[path] = routeHandlers; }, get() {} }; const calls = []; registerManageRoutes(app, { pool: { async query(sql) { if (sql.includes('SELECT s.slug') && sql.includes('FROM d_screens s') && sql.includes('ORDER BY s.slug ASC')) { return [[ { slug: 'alpha', } ]]; } return [[]]; } }, common: { fetchPlayerRegistrations: async () => ([ { public_base_url: 'http://player-a.example', internal_base_url: 'http://player-a.internal' } ]) }, pages: { renderScreenFormPage() {}, renderScreenEditPage() {} }, getAuditUserId() { return 7; }, redirectAfterSave() {}, notifyPlayerScreens: async () => 0, broadcastDashboardState: async () => { calls.push({ kind: 'broadcastDashboardState' }); }, getScreenDeleteBlockMessage: async () => '', getScreenConnections: async (slug) => { if (slug !== 'alpha') { return { connections: [] }; } return { connections: [ { id: 'alpha-1', clientId: 'alpha-1', deviceId: 'alpha-device', playerPublicBaseUrl: 'http://player-a.example/' } ] }; }, forwardPlayerCommand: async (slug, payload) => { calls.push({ kind: 'forwardPlayerCommand', slug, payload }); return { ok: true }; }, forwardPlayerCommandToBaseUrl: async (baseUrl, slug, payload) => { calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload }); return { ok: true }; }, requirePermission() { return function (_req, _res, next) { next(); }; } }); const routeHandlers = handlers['/commands']; assert.equal(Array.isArray(routeHandlers), true); const req = { body: { command: 'reload' }, query: {} }; const res = { statusCode: 200, body: null, status(code) { this.statusCode = code; return this; }, json(value) { this.body = value; return this; } }; await routeHandlers[1](req, res, () => {}); assert.equal(res.statusCode, 200); assert.equal(res.body.ok, true); assert.equal(res.body.sent, 1); assert.deepEqual( calls.filter((entry) => entry.kind === 'forwardPlayerCommandToBaseUrl').map((entry) => entry.baseUrl), ['http://player-a.internal'] ); assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand'), false); assert.equal(calls.some((entry) => entry.kind === 'broadcastDashboardState'), true); });