This commit is contained in:
190
packages/server/tests/local/ws-team-client.test.ts
Normal file
190
packages/server/tests/local/ws-team-client.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { WsTeamClient } from '../../src/local/ws-team-client.js';
|
||||
|
||||
/** Shape of the mock WebSocket the test inspects. */
|
||||
interface MockWs extends EventEmitter {
|
||||
readyState: number;
|
||||
sent: string[];
|
||||
send(data: string): void;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/** Typed accessor for the last mock WebSocket instance stashed on globalThis. */
|
||||
const globalWithMock = globalThis as typeof globalThis & {
|
||||
__lastMockWs: MockWs | null;
|
||||
};
|
||||
|
||||
// Mock the 'ws' module
|
||||
vi.mock('ws', async () => {
|
||||
const { EventEmitter } = await import('node:events');
|
||||
|
||||
class MockWebSocket extends EventEmitter {
|
||||
readyState = 1; // OPEN
|
||||
sent: string[] = [];
|
||||
|
||||
send(data: string) {
|
||||
this.sent.push(data);
|
||||
}
|
||||
|
||||
close() {
|
||||
this.readyState = 3; // CLOSED
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
// Store last instance for test access
|
||||
(globalThis as typeof globalThis & { __lastMockWs: MockWs | null }).__lastMockWs = null;
|
||||
|
||||
return {
|
||||
default: class extends MockWebSocket {
|
||||
constructor(_url: string) {
|
||||
super();
|
||||
(globalThis as typeof globalThis & { __lastMockWs: MockWs | null }).__lastMockWs = this;
|
||||
// Simulate async open
|
||||
setTimeout(() => this.emit('open'), 5);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
function getLastWs(): MockWs {
|
||||
const ws = globalWithMock.__lastMockWs;
|
||||
if (!ws) throw new Error('No mock WebSocket instance available');
|
||||
return ws;
|
||||
}
|
||||
|
||||
describe('WsTeamClient', () => {
|
||||
const config = {
|
||||
serverUrl: 'http://team.example.com',
|
||||
token: 'test-jwt-token',
|
||||
teamSlug: 'my-team',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
globalWithMock.__lastMockWs = null;
|
||||
});
|
||||
|
||||
it('stores config in constructor', () => {
|
||||
const client = new WsTeamClient(config);
|
||||
expect(client).toBeDefined();
|
||||
expect(client.isConnected()).toBe(false);
|
||||
});
|
||||
|
||||
it('sends authenticate on WebSocket open', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
await client.connect();
|
||||
|
||||
// Wait for 'open' event
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
expect(ws).toBeTruthy();
|
||||
expect(ws.sent.length).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const authMsg = JSON.parse(ws.sent[0]);
|
||||
expect(authMsg.type).toBe('authenticate');
|
||||
expect(authMsg.token).toBe('test-jwt-token');
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it('sends join_team after authenticated response', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
|
||||
// Simulate server responding with authenticated
|
||||
ws.emit('message', JSON.stringify({ type: 'authenticated', userId: 'user-123' }));
|
||||
|
||||
// Should have sent join_team
|
||||
const joinMsg = JSON.parse(ws.sent[ws.sent.length - 1]);
|
||||
expect(joinMsg.type).toBe('join_team');
|
||||
expect(joinMsg.teamSlug).toBe('my-team');
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it('emits message event on waggle_message', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
const messages: unknown[] = [];
|
||||
client.on('message', (msg) => messages.push(msg));
|
||||
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
|
||||
// Simulate waggle_message
|
||||
const testMessage = { type: 'request', subtype: 'knowledge_check', content: { query: 'test' } };
|
||||
ws.emit('message', JSON.stringify({ type: 'waggle_message', message: testMessage }));
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]).toEqual(testMessage);
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it('emits connected event on joined_team', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
const connected: Array<{ teamSlug?: string }> = [];
|
||||
client.on('connected', (data) => connected.push(data));
|
||||
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
|
||||
// Full protocol flow
|
||||
ws.emit('message', JSON.stringify({ type: 'authenticated', userId: 'user-123' }));
|
||||
ws.emit('message', JSON.stringify({ type: 'joined_team', teamSlug: 'my-team' }));
|
||||
|
||||
expect(connected).toHaveLength(1);
|
||||
expect(connected[0].teamSlug).toBe('my-team');
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it('isConnected returns true after authentication', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
|
||||
expect(client.isConnected()).toBe(false);
|
||||
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
ws.emit('message', JSON.stringify({ type: 'authenticated', userId: 'user-123' }));
|
||||
|
||||
expect(client.isConnected()).toBe(true);
|
||||
|
||||
client.disconnect();
|
||||
expect(client.isConnected()).toBe(false);
|
||||
});
|
||||
|
||||
it('disconnect stops reconnection and closes WebSocket', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
client.disconnect();
|
||||
|
||||
expect(client.isConnected()).toBe(false);
|
||||
});
|
||||
|
||||
it('handles invalid JSON messages gracefully', async () => {
|
||||
const client = new WsTeamClient(config);
|
||||
await client.connect();
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
|
||||
const ws = getLastWs();
|
||||
|
||||
// Should not throw
|
||||
ws.emit('message', 'not-json{{{');
|
||||
ws.emit('message', '');
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user