const path = require('path'); const fs = require('fs'); const os = require('os'); describe('load-admins', () => { const originalEnv = { ...process.env }; let tmpDir; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'admins-test-')); delete process.env.ADMIN_CONFIG_PATH; delete process.env.ADMIN_KEY; }); afterEach(() => { process.env = { ...originalEnv }; fs.rmSync(tmpDir, { recursive: true, force: true }); jest.resetModules(); }); function writeConfig(admins) { const filePath = path.join(tmpDir, 'admins.json'); fs.writeFileSync(filePath, JSON.stringify(admins)); return filePath; } test('loads admins from ADMIN_CONFIG_PATH', () => { const configPath = writeConfig([ { name: 'Alice', key: 'key-a' }, { name: 'Bob', key: 'key-b' } ]); process.env.ADMIN_CONFIG_PATH = configPath; const { findAdminByKey } = require('../../backend/config/load-admins'); expect(findAdminByKey('key-a')).toEqual({ name: 'Alice' }); expect(findAdminByKey('key-b')).toEqual({ name: 'Bob' }); expect(findAdminByKey('wrong')).toBeNull(); }); test('falls back to ADMIN_KEY when no config file', () => { process.env.ADMIN_CONFIG_PATH = path.join(tmpDir, 'nonexistent.json'); process.env.ADMIN_KEY = 'legacy-key'; const { findAdminByKey } = require('../../backend/config/load-admins'); expect(findAdminByKey('legacy-key')).toEqual({ name: 'Admin' }); expect(findAdminByKey('wrong')).toBeNull(); }); test('throws when neither config file nor ADMIN_KEY exists', () => { process.env.ADMIN_CONFIG_PATH = path.join(tmpDir, 'nonexistent.json'); expect(() => { require('../../backend/config/load-admins'); }).toThrow(); }); test('rejects duplicate admin names', () => { const configPath = writeConfig([ { name: 'Alice', key: 'key-a' }, { name: 'Alice', key: 'key-b' } ]); process.env.ADMIN_CONFIG_PATH = configPath; expect(() => { require('../../backend/config/load-admins'); }).toThrow(/duplicate/i); }); test('rejects duplicate keys', () => { const configPath = writeConfig([ { name: 'Alice', key: 'same-key' }, { name: 'Bob', key: 'same-key' } ]); process.env.ADMIN_CONFIG_PATH = configPath; expect(() => { require('../../backend/config/load-admins'); }).toThrow(/duplicate/i); }); }); const request = require('supertest'); describe('POST /api/auth/login — named admins', () => { let app; beforeAll(() => { process.env.ADMIN_KEY = 'test-admin-key'; process.env.ADMIN_CONFIG_PATH = '/tmp/nonexistent-admins.json'; jest.resetModules(); ({ app } = require('../../backend/server')); }); test('login returns admin name in response', async () => { const res = await request(app) .post('/api/auth/login') .set('Content-Type', 'application/json') .send({ key: 'test-admin-key' }); expect(res.status).toBe(200); expect(res.body.name).toBeDefined(); expect(res.body.token).toBeDefined(); }); test('verify returns admin name in user object', async () => { const loginRes = await request(app) .post('/api/auth/login') .set('Content-Type', 'application/json') .send({ key: 'test-admin-key' }); const res = await request(app) .post('/api/auth/verify') .set('Authorization', `Bearer ${loginRes.body.token}`); expect(res.status).toBe(200); expect(res.body.user.name).toBeDefined(); }); test('invalid key still returns 401', async () => { const res = await request(app) .post('/api/auth/login') .set('Content-Type', 'application/json') .send({ key: 'wrong-key' }); expect(res.status).toBe(401); }); }); const WebSocket = require('ws'); const jwt = require('jsonwebtoken'); const http = require('http'); describe('WebSocket presence', () => { let server, wsUrl; beforeAll((done) => { process.env.ADMIN_KEY = 'test-admin-key'; process.env.ADMIN_CONFIG_PATH = '/tmp/nonexistent-admins.json'; jest.resetModules(); const { app } = require('../../backend/server'); const { WebSocketManager, setWebSocketManager } = require('../../backend/utils/websocket-manager'); server = http.createServer(app); const wsManager = new WebSocketManager(server); setWebSocketManager(wsManager); server.listen(0, () => { const port = server.address().port; wsUrl = `ws://localhost:${port}/api/sessions/live`; done(); }); }); afterAll((done) => { server.close(done); }); function makeToken(name) { return jwt.sign({ role: 'admin', name }, process.env.JWT_SECRET, { expiresIn: '1h' }); } function connectAndAuth(name) { return new Promise((resolve) => { const ws = new WebSocket(wsUrl); ws.on('open', () => { ws.send(JSON.stringify({ type: 'auth', token: makeToken(name) })); }); ws.on('message', (data) => { const msg = JSON.parse(data.toString()); if (msg.type === 'auth_success') { resolve(ws); } }); }); } function waitForMessage(ws, type) { return new Promise((resolve) => { const handler = (data) => { const msg = JSON.parse(data.toString()); if (msg.type === type) { ws.off('message', handler); resolve(msg); } }; ws.on('message', handler); }); } test('page_focus triggers presence_update with admin name and page', async () => { const ws1 = await connectAndAuth('Alice'); const ws2 = await connectAndAuth('Bob'); const presencePromise = waitForMessage(ws2, 'presence_update'); ws1.send(JSON.stringify({ type: 'page_focus', page: '/history' })); const msg = await presencePromise; expect(msg.admins).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'Alice', page: '/history' }) ]) ); ws1.close(); ws2.close(); }); test('disconnect removes admin from presence', async () => { const ws1 = await connectAndAuth('Alice'); const ws2 = await connectAndAuth('Bob'); ws1.send(JSON.stringify({ type: 'page_focus', page: '/picker' })); ws2.send(JSON.stringify({ type: 'page_focus', page: '/picker' })); await new Promise(r => setTimeout(r, 100)); const presencePromise = waitForMessage(ws2, 'presence_update'); ws1.close(); const msg = await presencePromise; const names = msg.admins.map(a => a.name); expect(names).not.toContain('Alice'); ws2.close(); }); });