228 lines
6.4 KiB
JavaScript
228 lines
6.4 KiB
JavaScript
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();
|
|
});
|
|
});
|