import { test } from "node:test"; import assert from "node:assert/strict"; import { loadScenario } from "../src/declaration/parse.ts"; import { diagramFromDeclaration } from "../src/diagram/from-declaration.ts"; import { toDrawio } from "../src/diagram/drawio.ts"; /** * A generated diagram that will not open is worse than no diagram — it looks like a * deliverable and is not one. The first version was unparseable because HTML labels were * concatenated raw into an XML attribute, so these assert the file itself. */ function parseCells(xml: string): { id: string; vertex: boolean; edge: boolean; source?: string; target?: string }[] { const cells: ReturnType = []; // wrappers carry the id for any cell with a tooltip; the mxCell inside has none. for (const match of xml.matchAll(/]*?)>/g)) { const id = /id="([^"]*)"/.exec(match[1] ?? "")?.[1]; if (id) cells.push({ id, vertex: true, edge: false }); } for (const match of xml.matchAll(/]*?)(?:\/>|>)/g)) { const attrs = match[1] ?? ""; const get = (name: string) => new RegExp(`${name}="([^"]*)"`).exec(attrs)?.[1]; const id = get("id"); if (!id) continue; const entry: (typeof cells)[number] = { id, vertex: get("vertex") === "1", edge: get("edge") === "1", }; const source = get("source"); const target = get("target"); if (source) entry.source = source; if (target) entry.target = target; cells.push(entry); } return cells; } const scenario = loadScenario("scenarios/the-ordinary-shape.yml"); const xml = toDrawio(diagramFromDeclaration(scenario)); test("the file is well-formed XML — raw markup in an attribute is not", () => { // No unescaped angle bracket may appear inside a value="..." attribute. for (const match of xml.matchAll(/(?:value|label|tooltip)="([^"]*)"/g)) { assert.doesNotMatch(match[1] ?? "", /[<>]/, "a label carries raw markup into an attribute"); } assert.match(xml, /^\s*$/); }); test("every edge connects two cells that exist", () => { const cells = parseCells(xml); const ids = new Set(cells.map((c) => c.id)); for (const edge of cells.filter((c) => c.edge)) { assert.ok(ids.has(edge.source ?? ""), `edge ${edge.id} has no source`); assert.ok(ids.has(edge.target ?? ""), `edge ${edge.id} has no target`); } }); test("the diagram draws the implicit routers, not only what is written down", () => { // A scenario never names its gateways. A picture that showed only declared machines // would omit every node carrying NAT, forwarding and expiry. const diagram = diagramFromDeclaration(scenario); assert.ok(diagram.machines.some((m) => m.kind === "router"), "no router drawn"); assert.ok(diagram.machines.some((m) => m.kind === "transit"), "no transit drawn"); }); test("a router's badges say what the declaration decided", () => { const diagram = diagramFromDeclaration(scenario); const unforwardable = diagram.machines.find((m) => m.notes.some((n) => n.includes("NOT forwardable"))); assert.ok(unforwardable, "the unforwardable gateway is not marked as such"); assert.ok( diagram.machines.some((m) => m.notes.some((n) => n.includes("mappings expire"))), "a declared mapping expiry is not shown", ); }); test("a metadata fact becomes a badge, and absence of the fact does not", () => { // The point of the badges: the properties worth seeing are the ones with no visual // consequence. A translated address looks exactly like an untranslated one. for (const tip of ["translates v4", "no port forwarding", "mappings expire"]) { assert.ok(xml.includes(tip), `no badge explains '${tip}'`); } // A gateway that does not translate gets no mark, rather than a struck-through one. assert.doesNotMatch(xml, /label="N"[^>]*tooltip="[^"]*no NAT/); }); test("every badge says in words what its letter means", () => { // A one-letter code with no tooltip is a private language. Each badge is wrapped in an // , which is the only place draw.io reads a tooltip from. const badges = [...xml.matchAll(/]*label="([A-Z▶■]{1,2})"[^>]*tooltip="([^"]*)"/g)]; assert.ok(badges.length > 0, "no badges rendered at all"); for (const [, code, tip] of badges) { assert.ok((tip ?? "").length > 10, `badge ${code} has no explanation`); } }); test("no two cells share an id — a machine may be named after a gateway", () => { const ids = parseCells(xml).map((c) => c.id); assert.equal(new Set(ids).size, ids.length, "duplicate cell id"); }); test("segments are ordered public first, then by depth behind them", () => { const diagram = diagramFromDeclaration(scenario); const publicDepths = diagram.segments.filter((s) => s.kind === "public").map((s) => s.depth); assert.deepEqual([...new Set(publicDepths)], [0], "a public segment should be at depth 0"); const home = diagram.segments.find((s) => s.name === "home"); assert.equal(home?.depth, 1, "a segment behind one gateway is at depth 1"); }); test("a declared address appears on the machine that holds it", () => { assert.match(xml, /10\.99\.1\.135/); assert.match(xml, /198\.51\.100\.7/); }); test("every shape names a stencil that exists", () => { // A style naming a stencil draw.io does not have renders as an empty box — no error, no // warning, just a missing picture. Checked against the names in draw.io's own // stencils/networks.xml, which is where mxgraph.networks.* is defined. const KNOWN = new Set([ "mxgraph.networks.server", "mxgraph.networks.router", "mxgraph.networks.cloud", "mxgraph.networks.firewall", "mxgraph.networks.switch", "mxgraph.networks.pc", "mxgraph.networks.laptop", "mxgraph.networks.storage", "mxgraph.networks.modem", "mxgraph.networks.mainframe", ]); const used = new Set([...xml.matchAll(/shape=([a-z0-9_.]+)/g)].map((m) => m[1] as string)); assert.ok(used.size > 0, "no stencil shapes used at all"); for (const shape of used) { assert.ok(KNOWN.has(shape), `'${shape}' is not a stencil draw.io ships`); } }); test("a resource's shape is fixed by kind, and never varies with its metadata", () => { // The split the whole design rests on: shape says what a thing is, badges say what is // true about it. A gateway that stops translating must still look like a gateway. const routers = [...xml.matchAll(/shape=mxgraph\.networks\.router/g)].length; const diagram = diagramFromDeclaration(scenario); assert.equal(routers, diagram.machines.filter((m) => m.kind === "router").length); assert.equal( [...xml.matchAll(/shape=mxgraph\.networks\.server/g)].length, diagram.machines.filter((m) => m.kind === "machine").length, ); }); test("no link crosses a network it does not touch", () => { // The layout fault that made the first drawings unreadable: a gateway placed below the // lane it FACES, with its link to the outside running the height of the picture through // three networks it has nothing to do with — and overlapping another such link, so the // two read as one wire. Every link must now be short and local. const geometry = new Map(); for (const match of xml.matchAll( /<(?:mxCell|object)[^>]*id="([^"]*)"[\s\S]{0,400}? id.startsWith("lane-")) .map(([id, g]) => ({ name: id.slice(5), top: g.y, bottom: g.y + g.h })); assert.ok(lanes.length > 0, "no lanes found"); for (const edge of xml.matchAll(/]*edge="1"[^>]*source="([^"]*)" target="([^"]*)"/g)) { const [, , source, target] = edge; const from = geometry.get(source as string); const lane = lanes.find((l) => `lane-${l.name}` === target); if (!from || !lane) continue; const span = { top: Math.min(from.y, lane.top), bottom: Math.max(from.y + from.h, lane.bottom) }; const attached = new Set([target, source]); for (const other of lanes) { if (attached.has(`lane-${other.name}`)) continue; const crosses = other.top >= span.top && other.bottom <= span.bottom; assert.ok(!crosses, `link ${source}→${target} crosses '${other.name}'`); } } }); test("a network behind another is drawn inside it, not merely below it", () => { // "Behind" is shown by indentation. Without it the reader has only a wire to follow, and // in a layered scenario that wire is exactly what became unreadable. const laneX = new Map(); for (const match of xml.matchAll( / parent, `${segment.name} is not indented inside ${segment.behind}`); } }); test("a gateway sits immediately above the network it serves", () => { // What the grouping guarantees, and the whole reason a gateway reads as the door into a // network rather than a box floating near one. // // It does NOT guarantee adjacency to the network the gateway FACES: a public network with // two private networks behind it can only put one of them next to it. That case is carried // by the lane's own "behind …" and by both children being indented to the same depth — // and the link that would otherwise cross the sibling is suppressed, which is what the // crossing test above asserts. const diagram = diagramFromDeclaration(scenario); const order = [...xml.matchAll(/ m[1] as string); let checked = 0; for (const machine of diagram.machines) { if (machine.kind !== "router") continue; const served = machine.attachments.slice(1).map((a) => a.segment); const top = served.reduce((b, n) => (order.indexOf(n) < order.indexOf(b) ? n : b), served[0] ?? ""); assert.ok(order.includes(top), `${machine.name} serves '${top}', which is not drawn`); checked++; } assert.ok(checked > 0, "no gateways to check"); }); test("a public network is followed by everything behind it, before the next public one", () => { // The ordering rule itself. Sorting by depth alone put a private network far from the // public one it sits behind, which is what made the links long in the first place. const diagram = diagramFromDeclaration(scenario); const order = [...xml.matchAll(/ m[1] as string); const rootOf = (name: string): string => { let current = name; const seen = new Set(); for (;;) { const segment = diagram.segments.find((s) => s.name === current); if (!segment?.behind || seen.has(current)) return current; seen.add(current); current = segment.behind; } }; // Reading down the page, the root never returns to one already left behind. const finished = new Set(); let previous = ""; for (const name of order) { const root = rootOf(name); if (root !== previous) { assert.ok(!finished.has(root), `'${root}' is split apart by another group`); if (previous) finished.add(previous); previous = root; } } }); test("both sources lay the same topology out identically", () => { // The comparison is the feature. The hypervisor cannot know declaration order, so laying // out by it would give the two pictures different shapes and nothing could be read off // the difference. Proved by shuffling the segments and checking the layout does not move. const shuffled = { ...diagramFromDeclaration(scenario), segments: [...diagramFromDeclaration(scenario).segments].reverse(), }; const laneOrder = (out: string) => [...out.matchAll(/ m[1]); assert.deepEqual(laneOrder(toDrawio(shuffled)), laneOrder(xml)); }); const ALL_SCENARIOS = [ "bootstrap-single", "two-on-a-segment", "behind-nat", "segmented-and-unforwardable", "the-ordinary-shape", ]; for (const name of ALL_SCENARIOS) { test(`${name}: no box is drawn inside a network it is not on`, () => { // A gateway landed on top of a machine in an unrelated network, because the gap was // reserved above one sibling while the box was placed above the other. Two rules deciding // the same thing separately; both now read one map. const each = loadScenario(`scenarios/${name}.yml`); const out = toDrawio(diagramFromDeclaration(each)); const boxes: { id: string; y: number; h: number; lanes: string[] }[] = []; const lanes: { name: string; top: number; bottom: number }[] = []; for (const match of out.matchAll( /id="([^"]*)"[\s\S]{0,400}? { const box = boxes.find((b) => b.id === `m${index}`); if (box) box.lanes = machine.attachments.map((a) => a.segment); }); for (const box of boxes) { for (const lane of lanes) { if (box.lanes.includes(lane.name)) continue; const overlaps = box.y < lane.bottom && box.y + box.h > lane.top; assert.ok(!overlaps, `${box.id} is drawn inside '${lane.name}', which it is not on`); } } }); }