/** * Tests for {@link DataChannelManager}. Uses a structural * FakeRTCPeerConnection + FakeRTCDataChannel — no `vi.stubGlobal`, * no `vi.mock`. The Fakes are real `EventTarget` subclasses; tests * drive transitions through `simulate*` helpers. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import { WebRTCClient } from "../../client/client/client.js"; import { DRIVE_WEBRTC_CODES, DriveWebRtcError, } from "../../error-codes/error-codes.js"; import { DataChannelManager } from "./data-channel-manager.js"; class FakeRTCDataChannel extends EventTarget { public readyState: RTCDataChannelState = "connecting"; public bufferedAmount = 0; public bufferedAmountLowThreshold = 0; public closed = false; public sentPayloads: (ArrayBuffer | string)[] = []; public sendImpl: (payload: ArrayBuffer | string) => void = (payload) => { this.sentPayloads.push(payload); }; public closeImpl: () => void = () => { this.closed = true; this.readyState = "closed"; }; public constructor( public readonly label: string, _init?: RTCDataChannelInit ) { super(); } public send(payload: ArrayBuffer | string): void { this.sendImpl(payload); } public close(): void { this.closeImpl(); this.dispatchEvent(new Event("close")); } public simulateOpen(): void { this.readyState = "open"; this.dispatchEvent(new Event("open")); } public simulateClose(): void { this.readyState = "closed"; this.dispatchEvent(new Event("close")); } public simulateMessage(data: ArrayBuffer | string): void { const e = new Event("message") as MessageEvent; (e as unknown as { data: unknown }).data = data; this.dispatchEvent(e); } public simulateError(error?: Error): void { const e = new Event("error") as ErrorEvent; (e as unknown as { error: Error | undefined }).error = error; this.dispatchEvent(e); } public simulateBufferedAmount(value: number): void { this.bufferedAmount = value; } public simulateBufferedAmountLow(): void { this.dispatchEvent(new Event("bufferedamountlow")); } } class FakeRTCPeerConnection extends EventTarget { public connectionState: RTCPeerConnectionState = "new"; public iceConnectionState: RTCIceConnectionState = "new"; public localDescription: RTCSessionDescriptionInit | null = null; public remoteDescription: RTCSessionDescriptionInit | null = null; public createdChannels: FakeRTCDataChannel[] = []; public closed = false; public constructor(public readonly config?: RTCConfiguration) { super(); } public async createOffer(): Promise { return { type: "offer", sdp: "fake" }; } public async createAnswer(): Promise { return { type: "answer", sdp: "fake" }; } public async setLocalDescription( d: RTCSessionDescriptionInit ): Promise { this.localDescription = d; } public async setRemoteDescription( d: RTCSessionDescriptionInit ): Promise { this.remoteDescription = d; } public async addIceCandidate(): Promise {} public restartIce(): void {} public close(): void { this.closed = true; } public createDataChannel( label: string, init?: RTCDataChannelInit ): RTCDataChannel { const ch = new FakeRTCDataChannel(label, init); this.createdChannels.push(ch); return ch as unknown as RTCDataChannel; } } async function makeManager(opts?: { highWaterMarkBytes?: number; defaultLabel?: string; defaultOrdered?: boolean; }): Promise<{ manager: DataChannelManager; client: WebRTCClient; pc: FakeRTCPeerConnection; }> { const client = new WebRTCClient({ peerId: "self", config: { signaling: { url: "wss://x" } }, rtcPeerConnectionCtor: FakeRTCPeerConnection as unknown as typeof RTCPeerConnection, }); await client.createOffer("bob"); const pc = client.getPeerConnection( "bob" ) as unknown as FakeRTCPeerConnection; const manager = new DataChannelManager({ client, ...(opts?.highWaterMarkBytes !== undefined ? { highWaterMarkBytes: opts.highWaterMarkBytes } : {}), ...(opts?.defaultLabel !== undefined ? { defaultLabel: opts.defaultLabel } : {}), ...(opts?.defaultOrdered !== undefined ? { defaultOrdered: opts.defaultOrdered } : {}), }); return { manager, client, pc }; } describe("DataChannelManager.open", () => { it("throws PEER_NOT_FOUND for unknown peer", { timeout: 5000, }, async () => { const client = new WebRTCClient({ peerId: "self", config: { signaling: { url: "wss://x" } }, rtcPeerConnectionCtor: FakeRTCPeerConnection as unknown as typeof RTCPeerConnection, }); const manager = new DataChannelManager({ client }); try { manager.open("missing"); throw new Error("expected throw"); } catch (err) { expect(err).toBeInstanceOf(DriveWebRtcError); expect((err as DriveWebRtcError).code).toBe( DRIVE_WEBRTC_CODES.PEER_NOT_FOUND ); } }); it("creates a new channel on first open", { timeout: 5000 }, async () => { const { manager, pc } = await makeManager(); const ch = manager.open("bob"); expect(pc.createdChannels).toHaveLength(1); expect((ch as unknown as FakeRTCDataChannel).label).toBe( "drive-chunks" ); }); it("reuses an existing channel for the same label", { timeout: 5000, }, async () => { const { manager, pc } = await makeManager(); const a = manager.open("bob"); const b = manager.open("bob"); expect(a).toBe(b); expect(pc.createdChannels).toHaveLength(1); }); it("emits channel:open when channel opens", { timeout: 5000 }, async () => { const { manager } = await makeManager(); const events: { peerId: string; label: string }[] = []; manager.on("channel:open", (e) => events.push(e)); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); expect(events).toEqual([{ peerId: "bob", label: "drive-chunks" }]); }); it("emits channel:open eagerly when channel was already open at register", { timeout: 5000, }, async () => { const { manager, pc } = await makeManager(); // Forcefully open the channel before manager.open registers handlers. // We achieve this by monkey-patching createDataChannel for one call. const originalCreate = pc.createDataChannel.bind(pc); pc.createDataChannel = (label, init) => { const ch = originalCreate( label, init ) as unknown as FakeRTCDataChannel; ch.readyState = "open"; return ch as unknown as RTCDataChannel; }; const events: { peerId: string; label: string }[] = []; manager.on("channel:open", (e) => events.push(e)); manager.open("bob"); await Promise.resolve(); expect(events).toEqual([{ peerId: "bob", label: "drive-chunks" }]); }); it("threads RTCDataChannelInit", { timeout: 5000 }, async () => { const { manager, pc } = await makeManager({ defaultOrdered: false, }); manager.open("bob", "control", { maxRetransmits: 3 }); // Two channels created? No — only one for "control". expect(pc.createdChannels.map((c) => c.label)).toEqual(["control"]); }); }); describe("DataChannelManager.send", () => { it("throws DATA_CHANNEL_CLOSED if channel is missing", { timeout: 5000, }, async () => { const { manager } = await makeManager(); try { await manager.send("bob", "drive-chunks", "x"); throw new Error("expected throw"); } catch (err) { expect(err).toBeInstanceOf(DriveWebRtcError); expect((err as DriveWebRtcError).code).toBe( DRIVE_WEBRTC_CODES.DATA_CHANNEL_CLOSED ); } }); it("throws DATA_CHANNEL_CLOSED if channel not open", { timeout: 5000, }, async () => { const { manager } = await makeManager(); manager.open("bob"); try { await manager.send("bob", "drive-chunks", "x"); throw new Error("expected throw"); } catch (err) { expect((err as DriveWebRtcError).code).toBe( DRIVE_WEBRTC_CODES.DATA_CHANNEL_CLOSED ); } }); it("sends string payload when open and below watermark", { timeout: 5000, }, async () => { const { manager } = await makeManager({ highWaterMarkBytes: 1_000 }); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); await manager.send("bob", "drive-chunks", "hello"); expect(ch.sentPayloads).toEqual(["hello"]); }); it("sends ArrayBuffer payload", { timeout: 5000 }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const buf = new ArrayBuffer(4); await manager.send("bob", "drive-chunks", buf); expect(ch.sentPayloads[0]).toBe(buf); }); it("waits for bufferedamountlow when buffered > watermark", { timeout: 5000, }, async () => { const { manager } = await makeManager({ highWaterMarkBytes: 100 }); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); ch.simulateBufferedAmount(500); const pending = manager.send("bob", "drive-chunks", "p"); // Allow microtasks to run; pending must not yet have resolved. let resolved = false; pending.then(() => { resolved = true; }); await Promise.resolve(); expect(resolved).toBe(false); ch.simulateBufferedAmountLow(); await pending; expect(ch.sentPayloads).toEqual(["p"]); }); it("rejects with DATA_CHANNEL_CLOSED when AbortSignal aborts before send", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const ctrl = new AbortController(); ctrl.abort(new Error("boom")); try { await manager.send("bob", "drive-chunks", "p", ctrl.signal); throw new Error("expected throw"); } catch (err) { expect((err as DriveWebRtcError).code).toBe( DRIVE_WEBRTC_CODES.DATA_CHANNEL_CLOSED ); } expect(ch.sentPayloads).toEqual([]); }); it("rejects waiting send when AbortSignal aborts during drain", { timeout: 5000, }, async () => { const { manager } = await makeManager({ highWaterMarkBytes: 100 }); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); ch.simulateBufferedAmount(500); const ctrl = new AbortController(); const pending = manager.send("bob", "drive-chunks", "p", ctrl.signal); let rejection: unknown; pending.catch((err) => { rejection = err; }); await Promise.resolve(); ctrl.abort(new Error("user-cancel")); await Promise.resolve(); await Promise.resolve(); expect(rejection).toBeInstanceOf(DriveWebRtcError); expect((rejection as DriveWebRtcError).code).toBe( DRIVE_WEBRTC_CODES.DATA_CHANNEL_CLOSED ); }); }); describe("DataChannelManager.close / closeAll", () => { it("close(peerId, label) detaches and emits", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const closes: { peerId: string; label: string; reason?: string }[] = []; manager.on("channel:close", (e) => closes.push(e)); manager.close("bob", "drive-chunks", "test-reason"); expect(ch.closed).toBe(true); // Two close events: one from manager's emit + (NOT) one from the // simulateClose dispatch. Because detach happened before // channel.close, the close-listener already removed; only the // manager's own emit lands. expect(closes).toHaveLength(1); expect(closes[0]?.reason).toBe("test-reason"); expect(manager.get("bob")).toBeUndefined(); }); it("close(peerId) closes every label", { timeout: 5000 }, async () => { const { manager } = await makeManager(); const a = manager.open("bob", "a") as unknown as FakeRTCDataChannel; const b = manager.open("bob", "b") as unknown as FakeRTCDataChannel; a.simulateOpen(); b.simulateOpen(); manager.close("bob"); expect(a.closed).toBe(true); expect(b.closed).toBe(true); expect(manager.get("bob", "a")).toBeUndefined(); expect(manager.get("bob", "b")).toBeUndefined(); }); it("close is idempotent on unknown peer/label", { timeout: 5000, }, async () => { const { manager } = await makeManager(); manager.close("nobody"); manager.close("nobody", "x"); }); it("close with unknown label on known peer is a silent no-op", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); let closes = 0; manager.on("channel:close", () => closes++); manager.close("bob", "no-such-label"); expect(closes).toBe(0); // The known label is still alive. expect(manager.get("bob")).toBeDefined(); }); it("aborts drain wait with no signal reason", { timeout: 5000, }, async () => { const { manager } = await makeManager({ highWaterMarkBytes: 100 }); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); ch.simulateBufferedAmount(500); const ctrl = new AbortController(); const pending = manager.send("bob", "drive-chunks", "p", ctrl.signal); let rejection: unknown; pending.catch((err) => { rejection = err; }); await Promise.resolve(); ctrl.abort(); // no reason await Promise.resolve(); await Promise.resolve(); expect(rejection).toBeInstanceOf(DriveWebRtcError); }); it("closeAll closes every peer", { timeout: 5000 }, async () => { const client = new WebRTCClient({ peerId: "self", config: { signaling: { url: "wss://x" } }, rtcPeerConnectionCtor: FakeRTCPeerConnection as unknown as typeof RTCPeerConnection, }); await client.createOffer("bob"); await client.createOffer("carol"); const manager = new DataChannelManager({ client }); const a = manager.open("bob") as unknown as FakeRTCDataChannel; const b = manager.open("carol") as unknown as FakeRTCDataChannel; a.simulateOpen(); b.simulateOpen(); manager.closeAll("shutdown"); expect(a.closed).toBe(true); expect(b.closed).toBe(true); }); it("swallows close() throws", { timeout: 5000 }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); ch.closeImpl = () => { throw new Error("bad"); }; manager.close("bob"); }); }); describe("DataChannelManager remote events", () => { it("forwards messages", { timeout: 5000 }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const messages: { peerId: string; label: string; data: ArrayBuffer | string; }[] = []; manager.on("channel:message", (e) => messages.push(e)); ch.simulateMessage("hi"); const buf = new ArrayBuffer(2); ch.simulateMessage(buf); expect(messages).toHaveLength(2); expect(messages[0]?.data).toBe("hi"); expect(messages[1]?.data).toBe(buf); }); it("forwards errors with DriveWebRtcError(DATA_CHANNEL_CLOSED)", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const errors: { error: DriveWebRtcError }[] = []; manager.on("channel:error", (e) => errors.push(e)); ch.simulateError(new Error("boom")); ch.simulateError(undefined); expect(errors).toHaveLength(2); expect(errors[0]?.error.code).toBe( DRIVE_WEBRTC_CODES.DATA_CHANNEL_CLOSED ); expect(errors[0]?.error.cause).toBeInstanceOf(Error); expect(errors[1]?.error.cause).toBeUndefined(); }); it("emits channel:close when channel closes remotely", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob") as unknown as FakeRTCDataChannel; ch.simulateOpen(); const closes: { peerId: string; label: string }[] = []; manager.on("channel:close", (e) => closes.push(e)); ch.simulateClose(); expect(closes).toHaveLength(1); expect(manager.get("bob")).toBeUndefined(); }); }); describe("DataChannelManager.get", () => { it("returns the active channel by default label", { timeout: 5000, }, async () => { const { manager } = await makeManager(); const ch = manager.open("bob"); expect(manager.get("bob")).toBe(ch); }); it("returns undefined for unknown peer/label", { timeout: 5000, }, async () => { const { manager } = await makeManager(); expect(manager.get("nobody")).toBeUndefined(); manager.open("bob"); expect(manager.get("bob", "other")).toBeUndefined(); }); });