/** * V3.11 acceptance criterion: TURN-relay pÄtvinger relay-modus. * * We can't do real ICE in the memory factory, but we CAN verify that the * RTCConfiguration we pass to the underlying factory carries the * `iceTransportPolicy: 'relay'` flag through unchanged when the * application configures a TURN-only setup. This guarantees a real * RTCPeerConnection adapter (browser / wrtc / node-datachannel) will * reject all non-relay candidate pairs as the spec requires. */ import { afterEach, describe, expect, it } from 'bun:test'; import { MemoryShadeBridge, WebRtcSignalingChannel } from '../src/signaling.js'; import { WebRtcConnectionManager } from '../src/manager.js'; import { DEFAULT_STUN_SERVERS, type IDataChannel, type IPeerConnection, type IRtcFactory, type ShadeIceCandidate, type ShadeRtcConfig, type ShadeRtcConnectionState, type ShadeSessionDescription, } from '../src/types.js'; import { MemoryRtcFactory } from '../src/memory-rtc.js'; afterEach(() => { MemoryRtcFactory.reset(); }); class CapturingFactory implements IRtcFactory { configs: ShadeRtcConfig[] = []; constructor(private readonly inner: IRtcFactory) {} createPeerConnection(config: ShadeRtcConfig): IPeerConnection { this.configs.push(config); return this.inner.createPeerConnection(config); } } describe('TURN-relay configuration plumbing', () => { it('passes iceServers + iceTransportPolicy through to the underlying RTCConfiguration', async () => { const turnServers = [ { urls: 'turn:turn.example.com:3478', username: 'shade', credential: 'secret', }, ]; const inner = new MemoryRtcFactory(); const factory = new CapturingFactory(inner); const { a, b } = MemoryShadeBridge.linked('alice', 'bob'); const aliceSig = new WebRtcSignalingChannel(a); const bobSig = new WebRtcSignalingChannel(b); const alice = new WebRtcConnectionManager({ factory, signaling: aliceSig, config: { iceServers: turnServers, iceTransportPolicy: 'relay' }, }); const bob = new WebRtcConnectionManager({ factory, signaling: bobSig, config: { iceServers: turnServers, iceTransportPolicy: 'relay' }, receiver: { async onChunk() { return { lastSeq: 0 }; }, async onResumeQuery() { return null; }, }, }); await alice.getOrCreate('bob'); // Both sides created at least one PC; each call's config should carry // the TURN-only policy verbatim. expect(factory.configs.length).toBeGreaterThanOrEqual(2); for (const c of factory.configs) { expect(c.iceTransportPolicy).toBe('relay'); expect(c.iceServers).toEqual(turnServers); } alice.destroy(); bob.destroy(); }); it('falls back to default public STUN when no iceServers are supplied', async () => { const inner = new MemoryRtcFactory(); const factory = new CapturingFactory(inner); const { a, b } = MemoryShadeBridge.linked('alice', 'bob'); const alice = new WebRtcConnectionManager({ factory, signaling: new WebRtcSignalingChannel(a), defaultStunServers: DEFAULT_STUN_SERVERS, }); const bob = new WebRtcConnectionManager({ factory, signaling: new WebRtcSignalingChannel(b), defaultStunServers: DEFAULT_STUN_SERVERS, receiver: { async onChunk() { return { lastSeq: 0 }; }, async onResumeQuery() { return null; }, }, }); await alice.getOrCreate('bob'); for (const c of factory.configs) { expect(c.iceServers).toEqual(DEFAULT_STUN_SERVERS); } alice.destroy(); bob.destroy(); }); });