const test = require('node:test'); const assert = require('node:assert/strict'); require('../src/common'); const { createPageAuthToken } = require('../src/request-auth'); const { registerPlayerOnboardingRoutes } = require('../src/player/onboarding'); const originalSharedSecret = process.env.PULSE_SIGNAGE_SHARED_SECRET; const originalFetch = global.fetch; test.after(() => { if (originalSharedSecret === undefined) { delete process.env.PULSE_SIGNAGE_SHARED_SECRET; } else { process.env.PULSE_SIGNAGE_SHARED_SECRET = originalSharedSecret; } global.fetch = originalFetch; }); function createAppAndHandlers() { const handlers = {}; const app = { use() {}, get(path, ...routeHandlers) { handlers[path] = function (req, res) { return new Promise((resolve, reject) => { const runHandler = (index) => { const handler = routeHandlers[index]; if (!handler) { resolve(); return; } let nextCalled = false; const next = function () { nextCalled = true; return runHandler(index + 1); }; let result; try { result = handler(req, res, next); } catch (error) { reject(error); return; } Promise.resolve(result).then(function (value) { if (!nextCalled) { resolve(value); } }, reject); }; runHandler(0); }); }; }, post() {} }; return { app, handlers }; } function createResponse() { const headers = {}; return { headers, statusCode: 200, body: null, set(name, value) { headers[name] = value; return this; }, status(code) { this.statusCode = code; return this; }, type(value) { headers['Content-Type'] = value; return this; }, json(value) { this.body = value; return this; }, send(value) { this.body = value; return this; } }; } function registerThinClientRoutes(fetchImpl) { process.env.PULSE_SIGNAGE_SHARED_SECRET = 'bridge-secret'; global.fetch = fetchImpl; const { app, handlers } = createAppAndHandlers(); registerPlayerOnboardingRoutes(app, { app, common: { renderPlayerOnboardingLandingPage() { return 'landing'; }, renderPlayerOnboardingFormPage() { return 'form'; } }, thinClientBaseUrl: 'http://bridge.test', playerPublicBaseUrl: 'http://public.test' }); return handlers; } test('remote onboarding status proxies to the bridge and rewrites playerUrl', async () => { const fetchCalls = []; const handlers = registerThinClientRoutes(async (url, init) => { fetchCalls.push({ url, init }); return { status: 200, headers: { get(name) { return String(name || '').toLowerCase() === 'content-type' ? 'application/json; charset=utf-8' : null; } }, async json() { return { deviceId: 'device123', onboarded: true, clientName: 'Lobby Player', screenId: 7, screenSlug: 'main-screen', screenName: 'Main Screen', playerUrl: null }; } }; }); const token = createPageAuthToken({ scope: 'onboarding', deviceId: 'device123' }); const res = createResponse(); await handlers['/api/onboarding/status']({ headers: { 'x-pulse-page-auth': token }, query: { deviceId: 'device 123!?' }, socket: {} }, res); assert.equal(fetchCalls.length, 1); assert.equal(fetchCalls[0].url, 'http://bridge.test/api/onboarding/status?deviceId=device123'); assert.equal(fetchCalls[0].init.method, 'GET'); assert.equal(fetchCalls[0].init.headers['x-pulse-page-auth'], token); assert.equal(res.statusCode, 200); assert.equal(res.body.playerUrl, 'http://public.test/screen/main-screen'); assert.equal(res.body.screenSlug, 'main-screen'); }); test('remote onboarding status returns 502 when the bridge is unavailable', async () => { const handlers = registerThinClientRoutes(async () => { return null; }); const token = createPageAuthToken({ scope: 'player', deviceId: 'device123' }); const res = createResponse(); await handlers['/api/onboarding/status']({ headers: { 'x-pulse-page-auth': token }, query: { deviceId: 'device123' }, socket: {} }, res); assert.equal(res.statusCode, 502); assert.deepEqual(res.body, { error: 'Player bridge unavailable.' }); }); test('remote onboarding screens proxy preserves the bridge response body', async () => { const fetchCalls = []; const handlers = registerThinClientRoutes(async (url, init) => { fetchCalls.push({ url, init }); return { status: 200, headers: { get(name) { return String(name || '').toLowerCase() === 'content-type' ? 'application/json; charset=utf-8' : null; } }, async text() { return JSON.stringify({ screens: [{ id: 1, name: 'Lobby', slug: 'lobby' }] }); } }; }); const token = createPageAuthToken({ scope: 'onboarding', deviceId: 'device123' }); const res = createResponse(); await handlers['/api/onboarding/screens']({ headers: { 'x-pulse-page-auth': token }, query: {}, socket: {} }, res); assert.equal(fetchCalls.length, 1); assert.equal(fetchCalls[0].url, 'http://bridge.test/api/onboarding/screens'); assert.equal(fetchCalls[0].init.method, 'GET'); assert.equal(res.statusCode, 200); assert.equal(res.headers['Content-Type'], 'application/json; charset=utf-8'); assert.equal(res.body, JSON.stringify({ screens: [{ id: 1, name: 'Lobby', slug: 'lobby' }] })); });