Split web and player build artifacts
This commit is contained in:
@@ -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';
|
||||
}));
|
||||
});
|
||||
@@ -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';
|
||||
}));
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
Reference in New Issue
Block a user