Files
suaveplan/packages/networking/drive/drive-webrtc/src/data-channel/data-channel-manager/data-channel-manager.test.ts
2026-05-27 23:47:53 +01:00

518 lines
18 KiB
TypeScript

/**
* 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<RTCSessionDescriptionInit> {
return { type: "offer", sdp: "fake" };
}
public async createAnswer(): Promise<RTCSessionDescriptionInit> {
return { type: "answer", sdp: "fake" };
}
public async setLocalDescription(
d: RTCSessionDescriptionInit
): Promise<void> {
this.localDescription = d;
}
public async setRemoteDescription(
d: RTCSessionDescriptionInit
): Promise<void> {
this.remoteDescription = d;
}
public async addIceCandidate(): Promise<void> {}
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();
});
});