import { describe, it, expect, beforeEach, vi } from 'vitest'; import { WebSocket } from 'ws'; import { connectedUsers } from './state'; import { ConnectedUser } from './types'; import { handleWebSocketMessage } from './handler'; vi.mock('../services/server-access.service', () => ({ authorizeWebSocketJoin: vi.fn(async () => ({ allowed: true as const })), findServerMembership: vi.fn(async () => ({ id: 'membership-1' })), usersShareServerMembership: vi.fn(async () => false) })); vi.mock('../services/session-auth.service', () => ({ consumeSessionToken: vi.fn(async (token: string) => { if (token !== 'valid-token') { return null; } return { token, user: { id: 'user-1', username: 'alice', displayName: 'Alice', passwordHash: 'hash', createdAt: Date.now() }, issuedAt: Date.now(), expiresAt: Date.now() + 60_000 }; }) })); function createMockWs(): WebSocket & { sentMessages: string[] } { const sent: string[] = []; const ws = { readyState: WebSocket.OPEN, send: (data: string) => { sent.push(data); }, close: () => {}, sentMessages: sent } as unknown as WebSocket & { sentMessages: string[] }; return ws; } function createConnectedUser(connectionId: string): ConnectedUser { const ws = createMockWs(); const user: ConnectedUser = { oderId: connectionId, ws, authenticated: false, serverIds: new Set(), displayName: 'Test User', lastPong: Date.now() }; connectedUsers.set(connectionId, user); return user; } describe('server websocket handler - authentication', () => { beforeEach(() => { connectedUsers.clear(); vi.clearAllMocks(); }); it('rejects non-identify messages until the connection is authenticated', async () => { createConnectedUser('conn-1'); await handleWebSocketMessage('conn-1', { type: 'typing', serverId: 'server-1' }); const user = connectedUsers.get('conn-1'); const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages; const response = JSON.parse(sentMessages[0]) as { type: string }; expect(response.type).toBe('auth_required'); expect(user?.authenticated).toBe(false); }); it('rejects identify without a session token', async () => { createConnectedUser('conn-1'); await handleWebSocketMessage('conn-1', { type: 'identify', oderId: 'user-1', displayName: 'Alice' }); const user = connectedUsers.get('conn-1'); const sentMessages = (user?.ws as WebSocket & { sentMessages: string[] }).sentMessages; const response = JSON.parse(sentMessages[0]) as { type: string; code: string }; expect(response.type).toBe('auth_error'); expect(response.code).toBe('MISSING_TOKEN'); expect(user?.authenticated).toBe(false); }); it('binds identify to the authenticated user id from the token', async () => { createConnectedUser('conn-1'); await handleWebSocketMessage('conn-1', { type: 'identify', token: 'valid-token', oderId: 'user-1', displayName: 'Alice' }); const user = connectedUsers.get('conn-1'); expect(user?.authenticated).toBe(true); expect(user?.oderId).toBe('user-1'); }); });