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, /192\.168\.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, ); });