feat: WebSocket presence tracking with page_focus and presence_update
Made-with: Cursor
This commit is contained in:
@@ -125,3 +125,103 @@ describe('POST /api/auth/login — named admins', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user