658 lines
20 KiB
JavaScript
658 lines
20 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
require('../src/common');
|
|
|
|
const registerScreenCommandRoutes = require('../src/web/routes/admin/client-commands');
|
|
|
|
function createHandlers() {
|
|
const handlers = {};
|
|
const app = {
|
|
post(path, ...routeHandlers) {
|
|
handlers[path] = routeHandlers;
|
|
}
|
|
};
|
|
|
|
return { app, handlers };
|
|
}
|
|
|
|
test('move client rebinding redirects the live player to the target screen', async () => {
|
|
const calls = [];
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql, params) {
|
|
calls.push({ kind: 'query', sql, params });
|
|
|
|
if (sql.includes('SELECT slug FROM d_screens ORDER BY slug ASC')) {
|
|
return [[{ slug: 'source-screen' }, { slug: 'target-screen' }]];
|
|
}
|
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params && params[0] === 'source-screen') {
|
|
return [[{ id: 12, name: 'Source Screen', slug: 'source-screen' }]];
|
|
}
|
|
if (sql.includes('SELECT d.client_name, s.slug AS current_screen_slug')) {
|
|
return [[{ client_name: 'Stored Client Name', current_screen_slug: 'source-screen' }]];
|
|
}
|
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params && params[0] === 'target-screen') {
|
|
return [[{ id: 27, name: 'Target Screen', slug: 'target-screen' }]];
|
|
}
|
|
if (sql.includes('SELECT identifier, public_base_url FROM d_players WHERE public_base_url = ?')) {
|
|
return [[{ identifier: 'player-a', public_base_url: 'http://remote-player.example' }]];
|
|
}
|
|
if (sql.includes('INSERT INTO d_onboarding_devices')) {
|
|
return [{ affectedRows: 1 }];
|
|
}
|
|
|
|
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'
|
|
}
|
|
])
|
|
},
|
|
forwardPlayerCommand(slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommand', slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
forwardPlayerCommandToBaseUrl(baseUrl, slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
getScreenConnections: async (slug) => {
|
|
if (slug === 'source-screen') {
|
|
return {
|
|
connections: [
|
|
{
|
|
id: 'conn-1',
|
|
clientId: 'conn-1',
|
|
deviceId: 'device-123',
|
|
playerPublicBaseUrl: 'http://remote-player.example'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
if (slug === 'target-screen') {
|
|
return {
|
|
connections: [
|
|
{
|
|
id: 'target-conn-1',
|
|
clientId: 'target-conn-1',
|
|
deviceId: 'target-device-1',
|
|
playerPublicBaseUrl: 'https://remote-target.example'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
return { connections: [] };
|
|
},
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {
|
|
calls.push({ kind: 'broadcastDashboardState' });
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const routeHandlers = handlers['/clients/:slug/commands'];
|
|
assert.equal(Array.isArray(routeHandlers), true);
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1]({
|
|
params: { slug: 'source-screen' },
|
|
body: {
|
|
command: 'moveclient',
|
|
deviceId: 'device-123',
|
|
clientId: 'tab-123',
|
|
clientName: 'Lobby Client',
|
|
targetScreenSlug: 'target-screen',
|
|
connectionId: 'conn-1',
|
|
playerBaseUrl: 'http://remote-player.example'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.ok, true);
|
|
assert.equal(response.body.playerUrl, 'http://remote-player.example/screen/target-screen');
|
|
assert.equal(calls.some((entry) => entry.kind === 'query'
|
|
&& entry.sql.includes('SET screen_id = ?, modified_at = CURRENT_TIMESTAMP')
|
|
&& entry.params[0] === 27
|
|
&& entry.params[1] === 'device-123'), false);
|
|
assert.equal(calls.some((entry) => entry.kind === 'query'
|
|
&& entry.sql.includes('SET client_name = ?, screen_id = ?, modified_at = CURRENT_TIMESTAMP')
|
|
&& entry.params[0] === 'Lobby Client'
|
|
&& entry.params[1] === 27
|
|
&& entry.params[2] === 'tab-123'), true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'broadcastDashboardState'), true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand' && entry.slug === 'source-screen' && entry.payload && entry.payload.command === 'redirect'), false);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommandToBaseUrl' && entry.baseUrl === 'http://remote-player.example'), true);
|
|
});
|
|
|
|
test('move client requires a registered player identity', async () => {
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql) {
|
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?')) {
|
|
return [[{ id: 12, name: 'Source Screen', slug: 'source-screen' }]];
|
|
}
|
|
return [[]];
|
|
}
|
|
},
|
|
common: { fetchPlayerRegistrations: async () => [] },
|
|
forwardPlayerCommand: async () => ({ ok: true }),
|
|
forwardPlayerCommandToBaseUrl: async () => ({ ok: true }),
|
|
getScreenConnections: async () => ({ connections: [] }),
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await handlers['/clients/:slug/commands'][1]({
|
|
params: { slug: 'source-screen' },
|
|
body: {
|
|
command: 'moveclient',
|
|
deviceId: 'unregistered-device',
|
|
clientName: 'Lobby Client',
|
|
targetScreenSlug: 'target-screen',
|
|
playerBaseUrl: 'http://unregistered-player.example'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 400);
|
|
assert.equal(response.body.error, 'Registered player identity is required');
|
|
});
|
|
|
|
test('screen control commands can target all screens', async () => {
|
|
const calls = [];
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql) {
|
|
calls.push({ kind: 'query', sql });
|
|
|
|
if (String(sql || '').includes('FROM d_screens') && String(sql || '').includes('ORDER BY 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'
|
|
}
|
|
])
|
|
},
|
|
forwardPlayerCommand(slug, payload) {
|
|
calls.push({ kind: 'forwardPlayerCommand', slug, payload });
|
|
return { ok: true };
|
|
},
|
|
forwardPlayerCommandToBaseUrl(baseUrl, slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
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: [] };
|
|
},
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {
|
|
calls.push({ kind: 'broadcastDashboardState' });
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const routeHandlers = handlers['/clients/:slug/commands'];
|
|
assert.equal(Array.isArray(routeHandlers), true);
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1]({
|
|
params: { slug: '__all__' },
|
|
body: {
|
|
command: 'pause',
|
|
paused: 'true'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.ok, true);
|
|
assert.equal(response.body.allScreens, true);
|
|
assert.equal(response.body.targetScreenCount, 2);
|
|
assert.equal(response.body.targetPlayerCount, 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 === 'broadcastDashboardState'), true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand'), false);
|
|
});
|
|
|
|
test('screen control commands use the bridge for live connections', async () => {
|
|
const calls = [];
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql, params) {
|
|
calls.push({ kind: 'query', sql, params });
|
|
|
|
if (String(sql || '').includes('SELECT s.slug, s.player_id, p.public_base_url, p.internal_base_url')) {
|
|
return [[
|
|
{
|
|
slug: 'source-screen',
|
|
player_id: 'player-a',
|
|
public_base_url: 'http://player-a.example',
|
|
internal_base_url: 'http://player-a.internal'
|
|
}
|
|
]];
|
|
}
|
|
|
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params && params[0] === 'source-screen') {
|
|
return [[{ id: 12, name: 'Source Screen', slug: 'source-screen' }]];
|
|
}
|
|
|
|
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'
|
|
}
|
|
])
|
|
},
|
|
forwardPlayerCommand(slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommand', slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
getScreenConnections: async (slug) => {
|
|
if (slug === 'source-screen') {
|
|
return {
|
|
connections: [
|
|
{
|
|
id: 'conn-1',
|
|
clientId: 'conn-1',
|
|
deviceId: 'device-123',
|
|
playerPublicBaseUrl: 'http://local-player.example'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
return { connections: [] };
|
|
},
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {
|
|
calls.push({ kind: 'broadcastDashboardState' });
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const routeHandlers = handlers['/clients/:slug/commands'];
|
|
assert.equal(Array.isArray(routeHandlers), true);
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1]({
|
|
params: { slug: 'source-screen' },
|
|
body: {
|
|
command: 'pause',
|
|
connectionId: 'conn-1'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.ok, true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand' && entry.slug === 'source-screen'), true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommandToBaseUrl' && entry.baseUrl === 'http://local-player.example'), false);
|
|
});
|
|
|
|
test('screen control commands fan out through the bridge for the selected screen', async () => {
|
|
const calls = [];
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql, params) {
|
|
calls.push({ kind: 'query', sql, params });
|
|
|
|
if (String(sql || '').includes('SELECT s.slug, s.player_id, p.public_base_url, p.internal_base_url')) {
|
|
return [[{
|
|
slug: 'source-screen',
|
|
player_id: 'player-a',
|
|
public_base_url: 'http://player-a.example',
|
|
internal_base_url: 'http://player-a.internal'
|
|
}]];
|
|
}
|
|
|
|
if (sql.includes('SELECT id, name, slug FROM d_screens WHERE slug = ?') && params && params[0] === 'source-screen') {
|
|
return [[{ id: 12, name: 'Source Screen', slug: 'source-screen' }]];
|
|
}
|
|
|
|
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'
|
|
}
|
|
])
|
|
},
|
|
forwardPlayerCommand(slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommand', slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
forwardPlayerCommandToBaseUrl(baseUrl, slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
getScreenConnections: async (slug) => {
|
|
if (slug === 'source-screen') {
|
|
return {
|
|
connections: [
|
|
{
|
|
id: 'conn-1',
|
|
clientId: 'conn-1',
|
|
deviceId: 'device-123',
|
|
playerPublicBaseUrl: 'http://local-player.example'
|
|
},
|
|
{
|
|
id: 'conn-2',
|
|
clientId: 'conn-2',
|
|
deviceId: 'device-456',
|
|
playerPublicBaseUrl: 'http://local-player-b.example'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
return { connections: [] };
|
|
},
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {
|
|
calls.push({ kind: 'broadcastDashboardState' });
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const routeHandlers = handlers['/clients/:slug/commands'];
|
|
assert.equal(Array.isArray(routeHandlers), true);
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1]({
|
|
params: { slug: 'source-screen' },
|
|
body: {
|
|
command: 'pause'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.ok, true);
|
|
assert.equal(calls.some((entry) => entry.kind === 'forwardPlayerCommand' && entry.slug === 'source-screen'), false);
|
|
assert.deepEqual(
|
|
calls.filter((entry) => entry.kind === 'forwardPlayerCommandToBaseUrl').map((entry) => entry.baseUrl),
|
|
['http://local-player.example', 'http://local-player-b.example']
|
|
);
|
|
});
|
|
|
|
test('all screens commands fan out through the bridge for each screen', async () => {
|
|
const calls = [];
|
|
const { app, handlers } = createHandlers();
|
|
|
|
registerScreenCommandRoutes(app, {
|
|
pool: {
|
|
async query(sql) {
|
|
calls.push({ kind: 'query', sql });
|
|
|
|
if (String(sql || '').includes('FROM d_screens') && String(sql || '').includes('ORDER BY 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'
|
|
}
|
|
])
|
|
},
|
|
forwardPlayerCommand(slug, payload) {
|
|
calls.push({ kind: 'forwardPlayerCommand', slug, payload });
|
|
return { ok: true };
|
|
},
|
|
forwardPlayerCommandToBaseUrl(baseUrl, slug, payload, connectionId) {
|
|
calls.push({ kind: 'forwardPlayerCommandToBaseUrl', baseUrl, slug, payload, connectionId });
|
|
return { ok: true };
|
|
},
|
|
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: [] };
|
|
},
|
|
isClientNameAvailable: async () => true,
|
|
withClientNameReservation: async (_pool, _name, callback) => callback(),
|
|
broadcastDashboardState: async () => {
|
|
calls.push({ kind: 'broadcastDashboardState' });
|
|
},
|
|
requirePermission() {
|
|
return function (_req, _res, next) {
|
|
next();
|
|
};
|
|
}
|
|
});
|
|
|
|
const routeHandlers = handlers['/clients/:slug/commands'];
|
|
assert.equal(Array.isArray(routeHandlers), true);
|
|
|
|
const response = {
|
|
statusCode: 200,
|
|
body: null,
|
|
status(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json(value) {
|
|
this.body = value;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
await routeHandlers[1]({
|
|
params: { slug: '__all__' },
|
|
body: {
|
|
command: 'pause'
|
|
},
|
|
query: {}
|
|
}, response, () => {});
|
|
|
|
assert.equal(response.statusCode, 200);
|
|
assert.equal(response.body.ok, true);
|
|
assert.equal(response.body.allScreens, true);
|
|
assert.equal(response.body.targetScreenCount, 2);
|
|
assert.equal(response.body.targetPlayerCount, 2);
|
|
assert.equal(calls.some((entry) => entry.kind === 'broadcastDashboardState'), true);
|
|
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);
|
|
});
|