156 lines
5.0 KiB
TypeScript
156 lines
5.0 KiB
TypeScript
/**
|
|
* DMP-01, DMP-06, DMP-12 — `ChunkRequest` discriminated-union round-trip;
|
|
* `priority` round-trips through envelope encoding; response with
|
|
* unordered chunkIds is rejected.
|
|
*/
|
|
|
|
import { describe, expect, it } from "@suaveplan/testing/runner";
|
|
import {
|
|
chunkIdsToRawBitset,
|
|
DMP_CODES,
|
|
DmpError,
|
|
decodeRequestPayload,
|
|
decodeResponsePayload,
|
|
encodeFramedBitset,
|
|
encodeRequestPayload,
|
|
encodeResponsePayload,
|
|
selectWhole,
|
|
} from "../src/index.js";
|
|
import type {
|
|
ChunkRequestPayload,
|
|
ChunkResponsePayload,
|
|
} from "../src/types/protocol.types.js";
|
|
|
|
const FILE_ID = "a".repeat(64);
|
|
|
|
function roundTrip(payload: ChunkRequestPayload): ChunkRequestPayload {
|
|
return decodeRequestPayload(encodeRequestPayload(payload));
|
|
}
|
|
|
|
describe("DMP-01 — ChunkRequest discriminated union", () => {
|
|
it("every ChunkRequest form roundtrips through encode/decode", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const whole = roundTrip({
|
|
fileId: FILE_ID,
|
|
request: selectWhole(),
|
|
priority: "normal",
|
|
});
|
|
expect(whole.request.kind).toBe("whole");
|
|
|
|
const specific = roundTrip({
|
|
fileId: FILE_ID,
|
|
request: { kind: "specific", chunkIds: [0, 5, 17] },
|
|
priority: "normal",
|
|
});
|
|
expect(specific.request.kind).toBe("specific");
|
|
if (specific.request.kind === "specific") {
|
|
expect(specific.request.chunkIds).toEqual([0, 5, 17]);
|
|
}
|
|
|
|
const framed = encodeFramedBitset(chunkIdsToRawBitset([3, 7], 16), 16);
|
|
const bitset = roundTrip({
|
|
fileId: FILE_ID,
|
|
request: { kind: "bitset", bytes: framed },
|
|
priority: "high",
|
|
});
|
|
expect(bitset.request.kind).toBe("bitset");
|
|
if (bitset.request.kind === "bitset") {
|
|
expect(bitset.request.bytes).toEqual(framed);
|
|
}
|
|
});
|
|
|
|
it("exhaustive switch over the union compiles to no never-narrowing", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
const cover = (req: ChunkRequestPayload["request"]): string => {
|
|
switch (req.kind) {
|
|
case "whole":
|
|
return "whole";
|
|
case "specific":
|
|
return `specific:${req.chunkIds.length}`;
|
|
case "bitset":
|
|
return `bitset:${req.bytes.length}`;
|
|
}
|
|
};
|
|
expect(cover({ kind: "whole" })).toBe("whole");
|
|
});
|
|
});
|
|
|
|
describe("DMP-06 — priority round-trip", () => {
|
|
it("priority field round-trips through envelope encoding", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
for (const priority of [
|
|
"critical",
|
|
"high",
|
|
"normal",
|
|
"low",
|
|
"bulk",
|
|
] as const) {
|
|
const round = roundTrip({
|
|
fileId: FILE_ID,
|
|
request: selectWhole(),
|
|
priority,
|
|
});
|
|
expect(round.priority).toBe(priority);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("DMP-12 — response chunks ordered ascending", () => {
|
|
it("response with unordered chunkIds rejected", { timeout: 5000 }, () => {
|
|
const presence = encodeFramedBitset(chunkIdsToRawBitset([0, 1], 8), 8);
|
|
const payload: ChunkResponsePayload = {
|
|
fileId: FILE_ID,
|
|
chunks: [
|
|
{ chunkId: 1, bytes: new Uint8Array([1]) },
|
|
{ chunkId: 0, bytes: new Uint8Array([2]) },
|
|
],
|
|
presence,
|
|
generation: 1,
|
|
};
|
|
try {
|
|
encodeResponsePayload(payload);
|
|
throw new Error("expected unordered rejection");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.UNORDERED_RESPONSE);
|
|
}
|
|
});
|
|
|
|
it("decoder also rejects unordered chunks (defence-in-depth)", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
// Build an ordered envelope, then mutate the JSON to swap.
|
|
const presence = encodeFramedBitset(
|
|
chunkIdsToRawBitset([0, 1, 2], 8),
|
|
8
|
|
);
|
|
const ok: ChunkResponsePayload = {
|
|
fileId: FILE_ID,
|
|
chunks: [
|
|
{ chunkId: 0, bytes: new Uint8Array([10]) },
|
|
{ chunkId: 1, bytes: new Uint8Array([20]) },
|
|
],
|
|
presence,
|
|
generation: 5,
|
|
};
|
|
const wire = encodeResponsePayload(ok);
|
|
const text = new TextDecoder().decode(wire);
|
|
const obj = JSON.parse(text);
|
|
// Swap the chunkIds.
|
|
const tmp = obj.chunks[0].chunkId;
|
|
obj.chunks[0].chunkId = obj.chunks[1].chunkId;
|
|
obj.chunks[1].chunkId = tmp;
|
|
const tampered = new TextEncoder().encode(JSON.stringify(obj));
|
|
try {
|
|
decodeResponsePayload(tampered);
|
|
throw new Error("expected unordered rejection");
|
|
} catch (err) {
|
|
expect(err).toBeInstanceOf(DmpError);
|
|
expect((err as DmpError).code).toBe(DMP_CODES.UNORDERED_RESPONSE);
|
|
}
|
|
});
|
|
});
|