Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m12s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 30s
344 lines
9.4 KiB
JavaScript
344 lines
9.4 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
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);
|
|
}); |