Split web and player build artifacts

This commit is contained in:
2026-08-08 20:38:44 +01:00
parent 38565a533d
commit c3b5a0053c
33 changed files with 1113 additions and 140 deletions
+62
View File
@@ -0,0 +1,62 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const { registerInitialFontSyncTask } = require('../src/web/lib/background-tasks/tasks-startup/font-sync');
test('initial font sync queues a separate task for each live player', async () => {
const queuedTasks = [];
const liveLastSeenAt = new Date(Date.now() - 10_000).toISOString();
const staleLastSeenAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
await registerInitialFontSyncTask({
pool: {
async query() {
return [[
{
identifier: 'player-one',
public_base_url: 'https://player-one.example',
internal_base_url: 'http://player-one:8081',
last_seen_at: liveLastSeenAt
},
{
identifier: 'player-two',
public_base_url: 'https://player-two.example',
internal_base_url: 'http://player-two:8081',
last_seen_at: liveLastSeenAt
},
{
identifier: 'player-stale',
public_base_url: 'https://player-stale.example',
internal_base_url: 'http://player-stale:8081',
last_seen_at: staleLastSeenAt
}
]];
}
},
backgroundTaskQueue: {
async enqueueTask(definition) {
queuedTasks.push(definition);
return definition;
}
},
mediaDir: 'e:/Projects Git/pulse-signage/media',
uploadSyncService: {
async getPlayerTaskMetadata() {
return {};
}
}
});
assert.equal(queuedTasks.length, 2);
assert.deepEqual(queuedTasks.map(function (task) {
return task.metadata.playerIdentifier;
}).sort(), ['player-one', 'player-two']);
assert.deepEqual(queuedTasks.map(function (task) {
return task.payload.playerIdentifier;
}).sort(), ['player-one', 'player-two']);
assert.ok(queuedTasks.every(function (task) {
return task.key === 'initial-font-sync' && task.title === 'Initial font sync' && task.taskType === 'font-sync';
}));
});
+62
View File
@@ -0,0 +1,62 @@
const test = require('node:test');
const assert = require('node:assert/strict');
require('../src/common');
const { registerInitialMediaSyncTask } = require('../src/web/lib/background-tasks/tasks-startup/media-sync');
test('initial media sync queues a separate task for each live player', async () => {
const queuedTasks = [];
const liveLastSeenAt = new Date(Date.now() - 10_000).toISOString();
const staleLastSeenAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
await registerInitialMediaSyncTask({
pool: {
async query() {
return [[
{
identifier: 'player-one',
public_base_url: 'https://player-one.example',
internal_base_url: 'http://player-one:8081',
last_seen_at: liveLastSeenAt
},
{
identifier: 'player-two',
public_base_url: 'https://player-two.example',
internal_base_url: 'http://player-two:8081',
last_seen_at: liveLastSeenAt
},
{
identifier: 'player-stale',
public_base_url: 'https://player-stale.example',
internal_base_url: 'http://player-stale:8081',
last_seen_at: staleLastSeenAt
}
]];
}
},
backgroundTaskQueue: {
async enqueueTask(definition) {
queuedTasks.push(definition);
return definition;
}
},
mediaDir: 'e:/Projects Git/pulse-signage/media',
uploadSyncService: {
async getPlayerTaskMetadata() {
return {};
}
}
});
assert.equal(queuedTasks.length, 2);
assert.deepEqual(queuedTasks.map(function (task) {
return task.metadata.playerIdentifier;
}).sort(), ['player-one', 'player-two']);
assert.deepEqual(queuedTasks.map(function (task) {
return task.payload.playerIdentifier;
}).sort(), ['player-one', 'player-two']);
assert.ok(queuedTasks.every(function (task) {
return task.key === 'initial-media-sync' && task.title === 'Initial media sync' && task.taskType === 'media-sync';
}));
});
+158
View File
@@ -0,0 +1,158 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const express = require('express');
require('../src/common');
const { createRequestAuthHeaders } = require('../src/request-auth');
const registerInternalSyncRoutes = require('../src/web/routes/internal/sync');
test('player media sync requests are queued as background tasks', async () => {
const originalSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET;
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'test-shared-secret';
const queuedTasks = [];
const app = express();
app.use(express.json());
registerInternalSyncRoutes(app, {
backgroundTaskQueue: {
async enqueueTask(definition) {
queuedTasks.push(definition);
return {
id: 42,
key: definition.key,
taskType: definition.taskType,
title: definition.title,
category: definition.category,
status: 'queued',
createdAt: new Date().toISOString(),
startedAt: '',
finishedAt: '',
errorMessage: '',
attempts: 0,
metadata: definition.metadata,
payload: definition.payload
};
}
},
uploadSyncService: {},
mediaDir: 'e:/Projects Git/pulse-signage/media'
});
app.use(function (error, _req, res, _next) {
res.status(error.statusCode || 500).json({ error: String(error && error.message ? error.message : error) });
});
const server = app.listen(0);
try {
const address = server.address();
const body = {
deviceId: 'player-remote',
playerPublicBaseUrl: 'https://player.example.test',
playerInternalBaseUrl: 'http://player-bridge:8090'
};
const headers = Object.assign({
'content-type': 'application/json'
}, createRequestAuthHeaders({
method: 'POST',
pathname: '/api/internal/sync/player-media',
body: body
}));
const response = await fetch(`http://127.0.0.1:${address.port}/api/internal/sync/player-media`, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
});
assert.equal(response.status, 202);
assert.equal(queuedTasks.length, 1);
assert.equal(queuedTasks[0].key, 'player-media-sync');
assert.equal(queuedTasks[0].taskType, 'media-sync');
assert.equal(queuedTasks[0].payload.mode, 'initial');
assert.equal(queuedTasks[0].payload.playerIdentifier, 'player-remote');
assert.equal(queuedTasks[0].metadata.playerLabel, 'player-remote');
} finally {
await new Promise(function (resolve) {
server.close(resolve);
});
process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSecret;
}
});
test('player font sync requests are queued as background tasks', async () => {
const originalSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET;
process.env.PULSE_SIGNAGE_SHARED_SECRET = 'test-shared-secret';
const queuedTasks = [];
const app = express();
app.use(express.json());
registerInternalSyncRoutes(app, {
backgroundTaskQueue: {
async enqueueTask(definition) {
queuedTasks.push(definition);
return {
id: 43,
key: definition.key,
taskType: definition.taskType,
title: definition.title,
category: definition.category,
status: 'queued',
createdAt: new Date().toISOString(),
startedAt: '',
finishedAt: '',
errorMessage: '',
attempts: 0,
metadata: definition.metadata,
payload: definition.payload
};
}
},
uploadSyncService: {},
mediaDir: 'e:/Projects Git/pulse-signage/media'
});
app.use(function (error, _req, res, _next) {
res.status(error.statusCode || 500).json({ error: String(error && error.message ? error.message : error) });
});
const server = app.listen(0);
try {
const address = server.address();
const body = {
deviceId: 'player-remote',
playerPublicBaseUrl: 'https://player.example.test',
playerInternalBaseUrl: 'http://player-bridge:8090'
};
const headers = Object.assign({
'content-type': 'application/json'
}, createRequestAuthHeaders({
method: 'POST',
pathname: '/api/internal/sync/player-font',
body: body
}));
const response = await fetch(`http://127.0.0.1:${address.port}/api/internal/sync/player-font`, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
});
assert.equal(response.status, 202);
assert.equal(queuedTasks.length, 1);
assert.equal(queuedTasks[0].key, 'player-font-sync');
assert.equal(queuedTasks[0].taskType, 'font-sync');
assert.equal(queuedTasks[0].payload.mode, 'initial');
assert.equal(queuedTasks[0].payload.playerIdentifier, 'player-remote');
assert.equal(queuedTasks[0].metadata.playerLabel, 'player-remote');
} finally {
await new Promise(function (resolve) {
server.close(resolve);
});
process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSecret;
}
});
+13 -1
View File
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
require('../src/common');
const originalWebBaseUrl = process.env.WEB_INTERNAL_URL;
const { resolveWebBaseUrl, resolveScreenCommandTargets, resolveSnapshotUpstreamBaseUrl } = require('../src/player-bridge/index');
const { resolveWebBaseUrl, resolveScreenCommandTargets, resolveSnapshotUpstreamBaseUrl, resolvePlayerSocketForDeviceId } = require('../src/player-bridge/index');
test.after(() => {
if (originalWebBaseUrl === undefined) {
@@ -87,4 +87,16 @@ test('resolveSnapshotUpstreamBaseUrl falls back to the public player url for rem
public_base_url: 'https://remote-player.example',
internal_base_url: 'https://pulse-dev-bridge.lzstealth.com'
}), 'https://remote-player.example');
});
test('resolvePlayerSocketForDeviceId only returns the exact connected player socket', () => {
const playerSocketA = { readyState: 1 };
const playerSocketB = { readyState: 1 };
const playerSockets = new Map([
['player-a', playerSocketA],
['player-b', playerSocketB]
]);
assert.equal(resolvePlayerSocketForDeviceId(playerSockets, 'player-b'), playerSocketB);
assert.equal(resolvePlayerSocketForDeviceId(playerSockets, 'player-missing'), null);
});
+40
View File
@@ -97,6 +97,46 @@ function createPlayerRouteOptions(overrides) {
}, overrides);
}
test('screen route reports the request origin for the player public base url', async () => {
const { app, handlers } = createAppAndHandlers();
let reportedBaseUrl = null;
registerPlayerRoutes(app, {
app,
pool: { async query() { return [[{ id: 1 }]]; } },
common: { renderPlayerPage() { return '<html></html>'; } },
...createPlayerRouteOptions({
playerPlaylistService: {
async buildScreenPlaylist() {
return {
screen: { id: 7, slug: 'test2' },
playlist: null,
slides: [],
rssFeeds: [],
apiSources: [],
timetableGroups: [],
revision: 'abc123'
};
}
},
onPlayerPublicBaseUrl(value) {
reportedBaseUrl = value;
}
})
});
const handler = handlers['/screen/:slug'];
const res = createResponse();
await handler({
params: { slug: 'test2' },
headers: { host: '127.0.0.1:8080' },
socket: { encrypted: false }
}, res);
assert.equal(reportedBaseUrl, 'http://127.0.0.1:8080');
});
test('screen route falls back to offline rendering when playlist build fails', async () => {
const { app, handlers } = createAppAndHandlers();
const renderCalls = [];
+1 -1
View File
@@ -99,7 +99,7 @@ test('fqdn player registration wins over a local configured player target for me
assert.equal(success, true);
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://pulse-dev-bridge.lzstealth.com/api/media/uploads%2Fsample.bin');
assert.equal(fetchCalls[0].url, 'https://pulse-dev-bridge.lzstealth.com/api/media/uploads%2Fsample.bin?deviceId=player-remote');
} finally {
global.fetch = originalFetch;
fs.rmSync(uploadDir, { recursive: true, force: true });
+44
View File
@@ -0,0 +1,44 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const path = require('path');
const registerMiddleware = require('../src/web/middleware');
test('web middleware bypasses auth for internal player sync routes', () => {
const calls = [];
const app = {
middlewares: [],
use(...args) {
this.middlewares.push(args);
}
};
registerMiddleware(app, {
pool: {},
loadCurrentUser: async () => null,
requireAuth(req, _res, next) {
calls.push(req.path);
next();
},
MEDIA_DIR: path.join(process.cwd(), 'media'),
UPLOADS_DIR: path.join(process.cwd(), 'media', 'uploads-test'),
THUMBNAILS_DIR: path.join(process.cwd(), 'media', 'thumbnails-test'),
ASSET_DIR: path.join(process.cwd(), 'src', 'web', 'public')
});
const authGate = app.middlewares[app.middlewares.length - 1][0];
const nextCalls = [];
authGate({ path: '/api/internal/sync/player-media' }, {}, function () {
nextCalls.push('media');
});
authGate({ path: '/api/internal/sync/player-font' }, {}, function () {
nextCalls.push('font');
});
authGate({ path: '/dashboard' }, {}, function () {
nextCalls.push('dashboard');
});
assert.deepEqual(nextCalls, ['media', 'font', 'dashboard']);
assert.deepEqual(calls, ['/dashboard']);
});