mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-02 17:23:45 +00:00
- usePackets pulls a bigger first page (INITIAL_PACKET_PAGE_SIZE) - ModalOverlay + useFocusTrap, used by NodeDetailOverlay and PacketAnalyzerOverlay - PathData popovers for path resolutions; ObserverIcon and Tooltip - tests for packet structure and the focus trap; tidied types in api.ts/enums.ts
150 lines
5.7 KiB
TypeScript
150 lines
5.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { buildObservationFrame, computeFieldRanges } from "../../../src/features/packets/packet-structure";
|
|
import { PayloadType, RouteType } from "../../../src/types/enums";
|
|
import type { PacketDetail, Observation } from "../../../src/types/api";
|
|
|
|
// Minimal PacketDetail with a 1-byte header and a non-transport route, so the
|
|
// payload starts at byte offset 1 (no transport codes, and obs=null skips the path).
|
|
// parsedPayload here is a deliberately minimal stub: computeFieldRanges keys off the numeric
|
|
// header type and reads only top-level publicKey/signature plus the raw bytes, so these fixtures
|
|
// don't mirror the backend's full shape (which carries `type` and nests advert data under appData).
|
|
function makeDetail(opts: {
|
|
payloadType: number;
|
|
parsedPayload?: Record<string, unknown> | string;
|
|
rawPayload: string;
|
|
}): PacketDetail {
|
|
return {
|
|
packetHash: "deadbeef",
|
|
header: {
|
|
raw: "01",
|
|
routeType: RouteType.FLOOD,
|
|
routeTypeName: "FLOOD",
|
|
payloadType: opts.payloadType,
|
|
payloadTypeName: "x",
|
|
payloadVersion: 0,
|
|
},
|
|
parsedPayload: opts.parsedPayload,
|
|
rawPayload: opts.rawPayload,
|
|
decrypted: false,
|
|
firstHeardAt: 0,
|
|
lastHeardAt: 0,
|
|
firstToLastMs: 0,
|
|
observationCount: 0,
|
|
observations: [],
|
|
};
|
|
}
|
|
|
|
const rep = (byte: string, n: number) => byte.repeat(n);
|
|
|
|
function makeObs(opts: { raw: string; hashSize: number; hopCount: number; pathBytes?: string }): Observation {
|
|
return {
|
|
id: 1,
|
|
observerId: "obs-1",
|
|
iata: "YYZ",
|
|
heardAt: 0,
|
|
pathLength: { raw: opts.raw, hashSize: opts.hashSize, hopCount: opts.hopCount },
|
|
pathBytes: opts.pathBytes,
|
|
sourceBroker: "b",
|
|
resolvedPath: [],
|
|
};
|
|
}
|
|
|
|
function rangesFor(detail: PacketDetail) {
|
|
const frame = buildObservationFrame(detail, null);
|
|
return computeFieldRanges(detail, null, frame.length / 2);
|
|
}
|
|
|
|
describe("computeFieldRanges — ADVERT (lean backend shape, no `type` field)", () => {
|
|
it("colors public key, timestamp, signature, flags, location, and name", () => {
|
|
const publicKey = rep("aa", 32); // 32-byte key
|
|
const rawPayload =
|
|
publicKey + // 32B pubkey
|
|
"01020304" + // 4B timestamp
|
|
rep("bb", 64) + // 64B signature (omitted from the parsed payload)
|
|
"90" + // flags 0x90 = location(0x10) + name(0x80)
|
|
rep("cc", 8) + // 8B location
|
|
"4869"; // "Hi" (2B)
|
|
|
|
const ranges = rangesFor(
|
|
makeDetail({
|
|
payloadType: PayloadType.ADVERT,
|
|
parsedPayload: { publicKey, name: "Hi", nodeType: "ChatNode", timestamp: 1, lat: 1, lon: 2 },
|
|
rawPayload,
|
|
}),
|
|
);
|
|
|
|
expect(ranges.publicKey).toEqual({ start: 1, end: 33 });
|
|
expect(ranges.advertTimestamp).toEqual({ start: 33, end: 37 });
|
|
expect(ranges.signature).toEqual({ start: 37, end: 101 });
|
|
expect(ranges.flags).toEqual({ start: 101, end: 102 });
|
|
expect(ranges.location).toEqual({ start: 102, end: 110 });
|
|
expect(ranges.advertName).toEqual({ start: 110, end: 112 });
|
|
});
|
|
|
|
it("omits location and name when their flag bits are clear", () => {
|
|
const publicKey = rep("aa", 32);
|
|
const rawPayload = publicKey + "01020304" + rep("bb", 64) + "00"; // flags 0x00
|
|
|
|
const ranges = rangesFor(
|
|
makeDetail({
|
|
payloadType: PayloadType.ADVERT,
|
|
parsedPayload: { publicKey, timestamp: 1 },
|
|
rawPayload,
|
|
}),
|
|
);
|
|
|
|
expect(ranges.publicKey).toEqual({ start: 1, end: 33 });
|
|
expect(ranges.signature).toEqual({ start: 37, end: 101 });
|
|
expect(ranges.flags).toEqual({ start: 101, end: 102 });
|
|
expect(ranges.location).toBeUndefined();
|
|
expect(ranges.advertName).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("buildObservationFrame — path-length byte alignment", () => {
|
|
it("reserves a full path-length byte even when pathLength.raw is empty, so the payload stays aligned", () => {
|
|
// computeFieldRanges always reserves one byte for the path length when there's an observation,
|
|
// so the reconstructed frame must too — otherwise every field after the header shifts a byte.
|
|
const obs = makeObs({ raw: "", hashSize: 0, hopCount: 0 });
|
|
const detail = makeDetail({ payloadType: PayloadType.ANON_REQ, rawPayload: "aabbccdd" });
|
|
|
|
const frame = buildObservationFrame(detail, obs);
|
|
expect(frame).toBe("01" + "00" + "aabbccdd"); // header + path-length(00) + payload, nothing dropped
|
|
|
|
const ranges = computeFieldRanges(detail, obs, frame.length / 2);
|
|
expect(ranges.pathLength).toEqual({ start: 1, end: 2 });
|
|
expect(ranges.payload).toEqual({ start: 2, end: 6 }); // payload begins right after the path-length byte
|
|
});
|
|
|
|
it("pads a single-nibble path-length byte to two hex chars", () => {
|
|
const obs = makeObs({ raw: "e", hashSize: 0, hopCount: 0 });
|
|
const detail = makeDetail({ payloadType: PayloadType.ANON_REQ, rawPayload: "aabbccdd" });
|
|
|
|
expect(buildObservationFrame(detail, obs)).toBe("01" + "0e" + "aabbccdd");
|
|
});
|
|
});
|
|
|
|
describe("computeFieldRanges — ANON_REQ (lean backend shape, no `type` field)", () => {
|
|
it("colors destination, ephemeral key, MAC, and ciphertext", () => {
|
|
const ephemeralPubKey = rep("dd", 32);
|
|
const rawPayload =
|
|
"2a" + // 1B destination (42)
|
|
ephemeralPubKey + // 32B ephemeral pubkey
|
|
"eeee" + // 2B cipher MAC
|
|
rep("ff", 10); // 10B ciphertext
|
|
|
|
const ranges = rangesFor(
|
|
makeDetail({
|
|
payloadType: PayloadType.ANON_REQ,
|
|
parsedPayload: { destination: 42, ephemeralPubKey },
|
|
rawPayload,
|
|
}),
|
|
);
|
|
|
|
expect(ranges.destinationHash).toEqual({ start: 1, end: 2 });
|
|
expect(ranges.senderPublicKey).toEqual({ start: 2, end: 34 });
|
|
expect(ranges.cipherMac).toEqual({ start: 34, end: 36 });
|
|
expect(ranges.ciphertext).toEqual({ start: 36, end: 46 });
|
|
});
|
|
});
|