import { Injector, runInInjectionContext, signal } from '@angular/core'; import { Store } from '@ngrx/store'; import { Subject } from 'rxjs'; import { RealtimeSessionFacade } from '../../../../core/realtime'; import { selectAllUsers } from '../../../../store/users/users.selectors'; import type { ChatEvent, User } from '../../../../shared-kernel'; import { PeerDeliveryService } from './peer-delivery.service'; describe('PeerDeliveryService', () => { it('relays direct messages through signaling when no data channel is connected', () => { const context = createServiceContext({ connectedPeers: [], routedPeers: ['bob'] }); const event: ChatEvent = { type: 'direct-message', directMessage: { message: { id: 'message-1', conversationId: 'dm-alice-bob', senderId: 'alice', recipientId: 'bob', content: 'hello', timestamp: 1, status: 'QUEUED' }, sender: { userId: 'alice', username: 'alice', displayName: 'Alice' } } }; expect(context.service.sendViaWebRTC('bob', event)).toBe(true); expect(context.realtime.sendToPeer).not.toHaveBeenCalled(); expect(context.realtime.sendRawMessage).toHaveBeenCalledWith({ ...event, targetUserId: 'bob' }); }); it('keeps messages queued when neither P2P nor signaling can reach the recipient', () => { const context = createServiceContext({ connectedPeers: [], routedPeers: [] }); expect(context.service.sendViaWebRTC('bob', { type: 'direct-message' })).toBe(false); expect(context.realtime.sendRawMessage).not.toHaveBeenCalled(); }); it('emits direct messages received over signaling', () => { const context = createServiceContext({ connectedPeers: [] }); const received: ChatEvent[] = []; context.service.directMessageEvents$.subscribe((event) => received.push(event)); context.signalingMessages.next({ type: 'direct-message' } as ChatEvent); expect(received).toEqual([{ type: 'direct-message' }]); }); }); interface ServiceContextOptions { connectedPeers: string[]; routedPeers?: string[]; } interface ServiceContext { service: PeerDeliveryService; signalingMessages: Subject; realtime: { getConnectedPeers: ReturnType; hasSignalingRouteForPeer: ReturnType; sendRawMessage: ReturnType; sendToPeer: ReturnType; }; } function createServiceContext(options: ServiceContextOptions): ServiceContext { const users = signal([createUser('alice', 'Alice'), createUser('bob', 'Bob')]); const incomingMessages = new Subject(); const signalingMessages = new Subject(); const peerConnected = new Subject(); const realtime = { onMessageReceived: incomingMessages.asObservable(), onSignalingMessage: signalingMessages.asObservable(), onPeerConnected: peerConnected.asObservable(), getConnectedPeers: vi.fn(() => options.connectedPeers), hasSignalingRouteForPeer: vi.fn((peerId: string) => (options.routedPeers ?? []).includes(peerId)), sendRawMessage: vi.fn(), sendToPeer: vi.fn() }; const store = { selectSignal: vi.fn((selector: unknown) => { if (selector === selectAllUsers) { return users; } throw new Error('Unexpected selector requested by PeerDeliveryService test.'); }) }; const injector = Injector.create({ providers: [ { provide: RealtimeSessionFacade, useValue: realtime }, { provide: Store, useValue: store } ] }); return { service: runInInjectionContext(injector, () => new PeerDeliveryService()), signalingMessages, realtime }; } function createUser(id: string, displayName: string): User { return { id, oderId: id, username: displayName.toLowerCase(), displayName, status: 'online', role: 'member', joinedAt: 1 }; }