import { of } from 'rxjs'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Store } from '@ngrx/store'; import type { RealtimeSessionFacade } from '../../core/realtime'; import type { ServerDirectoryFacade } from '../../domains/server-directory'; import type { Room, User } from '../../shared-kernel'; import { RoomSignalingConnection } from './room-signaling-connection'; interface EndpointFixture { id: string; isActive: boolean; name: string; status: 'offline' | 'online'; url: string; } interface SignalSourceFixture { sourceId: string; sourceName?: string; sourceUrl: string; } interface SignalSelectorFixture { sourceId: string; sourceUrl: string; } interface FakeRealtimeSessionFacade { connectToSignalingServer: ReturnType; hasJoinedServer: ReturnType; identify: ReturnType; isSignalingConnectedTo: ReturnType; joinRoom: ReturnType; peerId: ReturnType; setCurrentServer: ReturnType; switchServer: ReturnType; } interface FakeServerDirectoryFacade { awaitInitialServerHealthCheck: ReturnType; buildRoomSignalSelector: ReturnType; ensureEndpointVersionCompatibility: ReturnType; findServerAcrossActiveEndpoints: ReturnType; getFallbackRoomEndpoints: ReturnType; getServer: ReturnType; getWebSocketUrl: ReturnType; normaliseRoomSignalSource: ReturnType; resolveRoomEndpoint: ReturnType; } interface FakeStore { dispatch: ReturnType; } describe('RoomSignalingConnection', () => { const room: Room = { id: 'room-1', name: 'Room One', description: '', hostId: 'user-1', createdAt: 1, userCount: 1, sourceId: 'primary', sourceName: 'Primary', sourceUrl: 'https://signal.toju.app' }; const user: User = { id: 'user-1', oderId: 'peer-a', username: 'maomao', displayName: 'maomao', status: 'online', createdAt: 1 }; let webrtc: FakeRealtimeSessionFacade; let serverDirectory: FakeServerDirectoryFacade; let store: FakeStore; beforeEach(() => { const endpoints = new Map([ [ 'primary', { id: 'primary', name: 'Primary', url: 'https://signal.toju.app', isActive: true, status: 'offline' } ], [ 'fallback', { id: 'fallback', name: 'Sweden', url: 'https://signal-sweden.toju.app', isActive: true, status: 'online' } ] ]); webrtc = { connectToSignalingServer: vi.fn((url: string) => of(url === 'wss://signal-sweden.toju.app')), hasJoinedServer: vi.fn(() => false), identify: vi.fn(), isSignalingConnectedTo: vi.fn(() => false), joinRoom: vi.fn(), peerId: vi.fn(() => 'peer-a'), setCurrentServer: vi.fn(), switchServer: vi.fn() }; serverDirectory = { awaitInitialServerHealthCheck: vi.fn(() => Promise.resolve()), buildRoomSignalSelector: vi.fn((source: SignalSourceFixture) => ({ sourceId: source.sourceId, sourceUrl: source.sourceUrl })), ensureEndpointVersionCompatibility: vi.fn((selector: SignalSelectorFixture) => Promise.resolve(selector.sourceId === 'fallback') ), findServerAcrossActiveEndpoints: vi.fn(() => of(null)), getFallbackRoomEndpoints: vi.fn(() => [endpoints.get('fallback')]), getServer: vi.fn(() => of(null)), getWebSocketUrl: vi.fn((selector: SignalSelectorFixture) => selector.sourceUrl.replace(/^http/, 'ws')), normaliseRoomSignalSource: vi.fn((source: SignalSourceFixture) => ({ sourceId: source.sourceId, sourceName: source.sourceName, sourceUrl: source.sourceUrl })), resolveRoomEndpoint: vi.fn((source: SignalSourceFixture) => endpoints.get(source.sourceId) ?? null) }; store = { dispatch: vi.fn() }; }); it('tries fallback endpoints when the primary endpoint is offline', async () => { const connection = new RoomSignalingConnection( webrtc as unknown as RealtimeSessionFacade, serverDirectory as unknown as ServerDirectoryFacade, store as unknown as Store ); connection.beginRoomNavigation(room.id); await connection.connectToRoomSignaling(room, user, user.oderId, [room]); expect(serverDirectory.ensureEndpointVersionCompatibility).toHaveBeenCalledTimes(2); expect(webrtc.connectToSignalingServer).toHaveBeenCalledWith('wss://signal-sweden.toju.app'); expect(webrtc.joinRoom).toHaveBeenCalledWith(room.id, user.oderId, 'wss://signal-sweden.toju.app'); }); });