431 lines
11 KiB
TypeScript
431 lines
11 KiB
TypeScript
/**
|
|
* Boundary schema tests for SignalingMessageSchema and its variants.
|
|
*
|
|
* Asserts (a) every variant parses, (b) malformed payloads are
|
|
* rejected, (c) unknown discriminators are rejected.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
AnnounceChunksMessageSchema,
|
|
AnswerMessageSchema,
|
|
ChunkBroadcastRequestSchema,
|
|
ChunkBroadcastResponseSchema,
|
|
ChunkDataMessageSchema,
|
|
ChunkDataPartMessageSchema,
|
|
ChunkQueryMessageSchema,
|
|
ChunkQueryResponseSchema,
|
|
ChunkRequestMessageSchema,
|
|
ChunksUpdatedMessageSchema,
|
|
DataChannelChunkRequestSchema,
|
|
IceCandidateMessageSchema,
|
|
OfferMessageSchema,
|
|
PeerJoinedMessageSchema,
|
|
PeerLeftMessageSchema,
|
|
RegisteredMessageSchema,
|
|
RegisterMessageSchema,
|
|
SignalingMessageSchema,
|
|
} from "../src/index.js";
|
|
|
|
const validVariants: Array<{ name: string; value: unknown }> = [
|
|
{
|
|
name: "offer",
|
|
value: {
|
|
type: "offer",
|
|
from: "a",
|
|
to: "b",
|
|
payload: { type: "offer", sdp: "v=0" },
|
|
},
|
|
},
|
|
{
|
|
name: "answer",
|
|
value: {
|
|
type: "answer",
|
|
payload: { type: "answer", sdp: "v=0" },
|
|
},
|
|
},
|
|
{
|
|
name: "ice-candidate",
|
|
value: {
|
|
type: "ice-candidate",
|
|
payload: { candidate: "candidate:1 1 UDP 1 1.1.1.1 1 typ host" },
|
|
},
|
|
},
|
|
{
|
|
name: "register",
|
|
value: {
|
|
type: "register",
|
|
payload: {
|
|
peerId: "p1",
|
|
peerName: "Alice",
|
|
authToken: "tok",
|
|
chunks: ["1:0", "1:1"],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "registered",
|
|
value: {
|
|
type: "registered",
|
|
payload: {
|
|
peerId: "p1",
|
|
peers: [{ id: "p2", name: "Bob", chunks: ["2:0"] }],
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "peer-joined",
|
|
value: {
|
|
type: "peer-joined",
|
|
payload: { id: "p3", name: "Carol", chunks: [] },
|
|
},
|
|
},
|
|
{
|
|
name: "peer-left",
|
|
value: { type: "peer-left", payload: { peerId: "p3" } },
|
|
},
|
|
{
|
|
name: "announce-chunks",
|
|
value: {
|
|
type: "announce-chunks",
|
|
payload: { chunks: ["1:0", "1:1"] },
|
|
},
|
|
},
|
|
{
|
|
name: "peer-chunks-updated",
|
|
value: {
|
|
type: "peer-chunks-updated",
|
|
payload: { peerId: "p1", chunks: ["1:0"] },
|
|
},
|
|
},
|
|
{
|
|
name: "request-chunk (signaling)",
|
|
value: {
|
|
type: "request-chunk",
|
|
payload: { requestId: "r1", fileId: "1", chunkId: "2" },
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-data",
|
|
value: {
|
|
type: "chunk-data",
|
|
payload: {
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
chunkId: "2",
|
|
data: "AAAA",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-data-part",
|
|
value: {
|
|
type: "chunk-data-part",
|
|
requestId: "r1",
|
|
partIndex: 0,
|
|
totalParts: 4,
|
|
data: "AAAA",
|
|
},
|
|
},
|
|
{
|
|
name: "request-chunk (data channel)",
|
|
value: {
|
|
type: "request-chunk",
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
chunkId: "2",
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-query",
|
|
value: {
|
|
type: "chunk-query",
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
chunkIds: ["0", "1"],
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-query-response",
|
|
value: {
|
|
type: "chunk-query-response",
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
availableChunkIds: ["0", "1"],
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-broadcast-request",
|
|
value: {
|
|
type: "chunk-broadcast-request",
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
neededChunkIds: ["0", "1"],
|
|
},
|
|
},
|
|
{
|
|
name: "chunk-broadcast-response",
|
|
value: {
|
|
type: "chunk-broadcast-response",
|
|
requestId: "r1",
|
|
fileId: "1",
|
|
peerId: "p1",
|
|
availableChunkIds: ["0", "1"],
|
|
},
|
|
},
|
|
];
|
|
|
|
describe("SignalingMessageSchema (union)", () => {
|
|
for (const { name, value } of validVariants) {
|
|
it(`accepts the ${name} variant`, { timeout: 5000 }, () => {
|
|
const r = SignalingMessageSchema.safeParse(value);
|
|
expect(r.success).toBe(true);
|
|
});
|
|
}
|
|
|
|
it("rejects unknown discriminator", { timeout: 5000 }, () => {
|
|
const r = SignalingMessageSchema.safeParse({
|
|
type: "totally-unknown",
|
|
payload: {},
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it("rejects fileId that is not a decimal string", { timeout: 5000 }, () => {
|
|
const r = SignalingMessageSchema.safeParse({
|
|
type: "chunk-query",
|
|
requestId: "r1",
|
|
fileId: "0xff",
|
|
chunkIds: ["0"],
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it("rejects negative partIndex on chunk-data-part", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const r = SignalingMessageSchema.safeParse({
|
|
type: "chunk-data-part",
|
|
requestId: "r1",
|
|
partIndex: -1,
|
|
totalParts: 4,
|
|
data: "AA",
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it("rejects zero totalParts on chunk-data-part", { timeout: 5000 }, () => {
|
|
const r = SignalingMessageSchema.safeParse({
|
|
type: "chunk-data-part",
|
|
requestId: "r1",
|
|
partIndex: 0,
|
|
totalParts: 0,
|
|
data: "AA",
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
|
|
it("rejects empty requestId", { timeout: 5000 }, () => {
|
|
const r = SignalingMessageSchema.safeParse({
|
|
type: "request-chunk",
|
|
requestId: "",
|
|
fileId: "1",
|
|
chunkId: "0",
|
|
});
|
|
expect(r.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("per-variant schemas", () => {
|
|
it("OfferMessageSchema rejects an answer", { timeout: 5000 }, () => {
|
|
expect(
|
|
OfferMessageSchema.safeParse({
|
|
type: "answer",
|
|
payload: { type: "answer" },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("AnswerMessageSchema rejects missing payload", { timeout: 5000 }, () => {
|
|
expect(AnswerMessageSchema.safeParse({ type: "answer" }).success).toBe(
|
|
false
|
|
);
|
|
});
|
|
|
|
it("IceCandidateMessageSchema accepts minimum payload", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
IceCandidateMessageSchema.safeParse({
|
|
type: "ice-candidate",
|
|
payload: {},
|
|
}).success
|
|
).toBe(true);
|
|
});
|
|
|
|
it("RegisterMessageSchema requires peerId", { timeout: 5000 }, () => {
|
|
expect(
|
|
RegisterMessageSchema.safeParse({
|
|
type: "register",
|
|
payload: { peerName: "n", chunks: [] },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("RegisteredMessageSchema requires peers array", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
RegisteredMessageSchema.safeParse({
|
|
type: "registered",
|
|
payload: { peerId: "p1" },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("PeerJoinedMessageSchema requires id", { timeout: 5000 }, () => {
|
|
expect(
|
|
PeerJoinedMessageSchema.safeParse({
|
|
type: "peer-joined",
|
|
payload: { name: "n", chunks: [] },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("PeerLeftMessageSchema rejects missing peerId", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
PeerLeftMessageSchema.safeParse({
|
|
type: "peer-left",
|
|
payload: {},
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("AnnounceChunksMessageSchema accepts empty chunks array", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
AnnounceChunksMessageSchema.safeParse({
|
|
type: "announce-chunks",
|
|
payload: { chunks: [] },
|
|
}).success
|
|
).toBe(true);
|
|
});
|
|
|
|
it("ChunksUpdatedMessageSchema rejects empty peerId", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunksUpdatedMessageSchema.safeParse({
|
|
type: "peer-chunks-updated",
|
|
payload: { peerId: "", chunks: [] },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkRequestMessageSchema rejects empty data base64", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkDataMessageSchema.safeParse({
|
|
type: "chunk-data",
|
|
payload: {
|
|
requestId: "r",
|
|
fileId: "1",
|
|
chunkId: "0",
|
|
data: "",
|
|
},
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkRequestMessageSchema requires nested payload", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkRequestMessageSchema.safeParse({
|
|
type: "request-chunk",
|
|
requestId: "r",
|
|
fileId: "1",
|
|
chunkId: "0",
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("DataChannelChunkRequestSchema requires flat fields", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
DataChannelChunkRequestSchema.safeParse({
|
|
type: "request-chunk",
|
|
payload: { requestId: "r", fileId: "1", chunkId: "0" },
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkDataPartMessageSchema rejects float partIndex", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkDataPartMessageSchema.safeParse({
|
|
type: "chunk-data-part",
|
|
requestId: "r",
|
|
partIndex: 0.5,
|
|
totalParts: 4,
|
|
data: "x",
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkQueryMessageSchema accepts missing chunkIds", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkQueryMessageSchema.safeParse({
|
|
type: "chunk-query",
|
|
requestId: "r",
|
|
fileId: "1",
|
|
}).success
|
|
).toBe(true);
|
|
});
|
|
|
|
it("ChunkQueryResponseSchema requires availableChunkIds", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkQueryResponseSchema.safeParse({
|
|
type: "chunk-query-response",
|
|
requestId: "r",
|
|
fileId: "1",
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkBroadcastRequestSchema requires neededChunkIds", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkBroadcastRequestSchema.safeParse({
|
|
type: "chunk-broadcast-request",
|
|
requestId: "r",
|
|
fileId: "1",
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
|
|
it("ChunkBroadcastResponseSchema requires peerId", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
expect(
|
|
ChunkBroadcastResponseSchema.safeParse({
|
|
type: "chunk-broadcast-response",
|
|
requestId: "r",
|
|
fileId: "1",
|
|
availableChunkIds: [],
|
|
}).success
|
|
).toBe(false);
|
|
});
|
|
});
|