Initial Commit
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user