366 lines
11 KiB
JavaScript
366 lines
11 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const express = require('express');
|
|
|
|
require('../src/common');
|
|
|
|
const { createUploadSyncService } = require('../src/web/lib/media');
|
|
|
|
test('multipart uploads accept large text fields used by slide and template forms', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-'));
|
|
const uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {}
|
|
});
|
|
const upload = uploadSyncService.createUploadMiddleware(uploadDir);
|
|
const app = express();
|
|
|
|
app.post('/upload', upload.any(), function (_req, res) {
|
|
res.sendStatus(204);
|
|
});
|
|
|
|
app.use(function (error, _req, res, _next) {
|
|
res.status(error.statusCode || 500).send(String(error && error.message ? error.message : error));
|
|
});
|
|
|
|
const server = app.listen(0);
|
|
|
|
try {
|
|
const address = server.address();
|
|
const largeValue = 'x'.repeat(1_100_000);
|
|
const formData = new FormData();
|
|
formData.set('region_text_6', largeValue);
|
|
|
|
const response = await fetch(`http://127.0.0.1:${address.port}/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
assert.equal(response.status, 204);
|
|
} finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('fqdn player registration wins over a local configured player target for media sync', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-'));
|
|
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
|
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
|
const activeLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
|
|
|
const fetchCalls = [];
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async function (url, init) {
|
|
fetchCalls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: {
|
|
get() {
|
|
return null;
|
|
}
|
|
},
|
|
async text() {
|
|
return JSON.stringify({ ok: true });
|
|
}
|
|
};
|
|
};
|
|
|
|
const uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
pool: {
|
|
async query() {
|
|
return [[
|
|
{
|
|
identifier: 'player-local',
|
|
internal_base_url: 'http://player:8081',
|
|
last_seen_at: activeLastSeenAt
|
|
},
|
|
{
|
|
identifier: 'player-remote',
|
|
internal_base_url: 'https://pulse-dev-bridge.lzstealth.com',
|
|
last_seen_at: activeLastSeenAt
|
|
}
|
|
]];
|
|
}
|
|
},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {}
|
|
});
|
|
|
|
try {
|
|
const success = await uploadSyncService.pushUploadFileToPlayer('/media/uploads/sample.bin', uploadDir);
|
|
|
|
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?deviceId=player-remote');
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('media sync retry warnings include the resolved player label', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-log-'));
|
|
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
|
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
|
const activeLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
|
|
|
const originalFetch = global.fetch;
|
|
const originalWarn = console.warn;
|
|
const warned = [];
|
|
console.warn = function () {
|
|
warned.push(Array.from(arguments).join(' '));
|
|
};
|
|
global.fetch = async function () {
|
|
const error = new Error('getaddrinfo ENOTFOUND player-remote');
|
|
error.code = 'ENOTFOUND';
|
|
throw error;
|
|
};
|
|
|
|
const uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
pool: {
|
|
async query() {
|
|
return [[
|
|
{
|
|
identifier: 'player-remote',
|
|
internal_base_url: 'https://pulse-dev-bridge.lzstealth.com',
|
|
last_seen_at: activeLastSeenAt
|
|
}
|
|
]];
|
|
}
|
|
},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {}
|
|
});
|
|
|
|
try {
|
|
await uploadSyncService.syncUploadRefsToPlayer(['/media/uploads/sample.bin'], uploadDir);
|
|
await uploadSyncService.flushPendingPlayerUploadSyncs();
|
|
|
|
assert.ok(warned.some(function (message) {
|
|
return message.includes('[media-sync] Player unavailable, retry queued for 1 upload for player-remote');
|
|
}));
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
console.warn = originalWarn;
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('media sync treats 503 responses as unavailable without logging a per-upload warning', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-503-'));
|
|
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
|
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
|
const activeLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
|
|
|
const originalFetch = global.fetch;
|
|
const originalWarn = console.warn;
|
|
const warned = [];
|
|
console.warn = function () {
|
|
warned.push(Array.from(arguments).join(' '));
|
|
};
|
|
global.fetch = async function () {
|
|
return {
|
|
ok: false,
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: {
|
|
get() {
|
|
return null;
|
|
}
|
|
},
|
|
async text() {
|
|
return 'Service Unavailable';
|
|
}
|
|
};
|
|
};
|
|
|
|
const uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
pool: {
|
|
async query() {
|
|
return [[
|
|
{
|
|
identifier: 'player-remote',
|
|
internal_base_url: 'https://pulse-dev-bridge.lzstealth.com',
|
|
last_seen_at: activeLastSeenAt
|
|
}
|
|
]];
|
|
}
|
|
},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {}
|
|
});
|
|
|
|
try {
|
|
await uploadSyncService.syncUploadRefsToPlayer(['/media/uploads/sample.bin'], uploadDir);
|
|
await uploadSyncService.flushPendingPlayerUploadSyncs();
|
|
|
|
assert.ok(warned.some(function (message) {
|
|
return message.includes('[media-sync] Player unavailable, retry queued for 1 upload for player-remote');
|
|
}));
|
|
assert.ok(warned.every(function (message) {
|
|
return !message.includes('Unable to sync upload to player: sample.bin 503 Service Unavailable');
|
|
}));
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
console.warn = originalWarn;
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('stale player registrations stop media sync retries and warnings', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-stale-'));
|
|
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
|
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
|
|
|
const originalFetch = global.fetch;
|
|
const originalWarn = console.warn;
|
|
const fetchCalls = [];
|
|
const warned = [];
|
|
console.warn = function () {
|
|
warned.push(Array.from(arguments).join(' '));
|
|
};
|
|
global.fetch = async function (url, init) {
|
|
fetchCalls.push({ url, init });
|
|
return {
|
|
ok: false,
|
|
status: 503,
|
|
statusText: 'Service Unavailable',
|
|
headers: {
|
|
get() {
|
|
return null;
|
|
}
|
|
},
|
|
async text() {
|
|
return 'Service Unavailable';
|
|
}
|
|
};
|
|
};
|
|
|
|
const uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
pool: {
|
|
async query() {
|
|
return [[
|
|
{
|
|
identifier: 'player-remote',
|
|
internal_base_url: 'https://pulse-dev-bridge.lzstealth.com',
|
|
last_seen_at: new Date(Date.now() - 5 * 60 * 1000).toISOString()
|
|
}
|
|
]];
|
|
}
|
|
},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {}
|
|
});
|
|
|
|
try {
|
|
await uploadSyncService.syncUploadRefsToPlayer(['/media/uploads/sample.bin'], uploadDir);
|
|
await uploadSyncService.flushPendingPlayerUploadSyncs();
|
|
|
|
assert.equal(fetchCalls.length, 0);
|
|
assert.equal(warned.length, 0);
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
console.warn = originalWarn;
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('slide update sync queues one media task per live player and targets each player base url', async () => {
|
|
const uploadDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulse-signage-upload-sync-multi-'));
|
|
fs.mkdirSync(path.join(uploadDir, 'uploads'), { recursive: true });
|
|
fs.writeFileSync(path.join(uploadDir, 'uploads', 'sample.bin'), Buffer.from('hello world'));
|
|
const liveLastSeenAt = new Date(Date.now() - 10_000).toISOString();
|
|
const staleLastSeenAt = new Date(Date.now() - 5 * 60 * 1000).toISOString();
|
|
|
|
const fetchCalls = [];
|
|
const queuedTasks = [];
|
|
const originalFetch = global.fetch;
|
|
global.fetch = async function (url, init) {
|
|
fetchCalls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
headers: {
|
|
get() {
|
|
return null;
|
|
}
|
|
},
|
|
async text() {
|
|
return JSON.stringify({ ok: true });
|
|
}
|
|
};
|
|
};
|
|
|
|
let uploadSyncService;
|
|
uploadSyncService = createUploadSyncService({
|
|
common: {},
|
|
pool: {
|
|
async query() {
|
|
return [[
|
|
{
|
|
identifier: 'player-one',
|
|
internal_base_url: 'http://player-one:8081',
|
|
last_seen_at: liveLastSeenAt
|
|
},
|
|
{
|
|
identifier: 'player-two',
|
|
internal_base_url: 'http://player-two:8081',
|
|
last_seen_at: liveLastSeenAt
|
|
},
|
|
{
|
|
identifier: 'player-stale',
|
|
internal_base_url: 'http://player-stale:8081',
|
|
last_seen_at: staleLastSeenAt
|
|
}
|
|
]];
|
|
}
|
|
},
|
|
playerSnapshotCache: new Map(),
|
|
notifyPlayerScreens: async () => {},
|
|
backgroundTaskQueue: {
|
|
async enqueueTaskAndWait(definition) {
|
|
queuedTasks.push(definition);
|
|
await uploadSyncService.runMediaSyncTask(definition.payload);
|
|
return definition;
|
|
}
|
|
}
|
|
});
|
|
|
|
try {
|
|
await uploadSyncService.syncPlaylistUploadsOnChange({
|
|
key: 'slide:update:123',
|
|
pool: {},
|
|
localUploadDir: uploadDir,
|
|
nextUploadRefs: ['/media/uploads/sample.bin']
|
|
});
|
|
|
|
assert.equal(queuedTasks.length, 2);
|
|
assert.deepEqual(queuedTasks.map(function (task) {
|
|
return task.metadata.playerIdentifier;
|
|
}).sort(), ['player-one', 'player-two']);
|
|
assert.ok(queuedTasks.every(function (task) {
|
|
return task.key.startsWith('media-sync:slide:update:123:');
|
|
}));
|
|
assert.deepEqual(fetchCalls.map(function (call) {
|
|
return call.url;
|
|
}).sort(), [
|
|
'http://player-one:8081/api/media/uploads%2Fsample.bin?deviceId=player-one',
|
|
'http://player-two:8081/api/media/uploads%2Fsample.bin?deviceId=player-two'
|
|
].sort());
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
fs.rmSync(uploadDir, { recursive: true, force: true });
|
|
}
|
|
}); |