// The viewers of a running simulation in the room, on FILIUS' example // nat_with_mapped_port: a switch's table and a home router's NAT table as // views (their reset an action), and "Pakete verwerfen": every cable drops // while any client holds it, and the dropped frames go to who watches them, // begun anew with each simulation. The tables' rows against Java's are // packages/core/test/viewers.test.ts. import { readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; import * as Y from 'yjs'; import { readProject, setSoftware, writeProject } from '@mdg-filius/doc'; import { openProjectArchive } from '@mdg-filius/fls'; import type { DroppedUnit, HostMessage, NatView, SatView } from '@mdg-filius/protocol'; import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js'; const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus/java-examples'); function setUp() { let counter = 0; const doc = new Y.Doc(); writeProject( doc, openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'nat_with_mapped_port.fls'))), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`), ); const project = readProject(doc); const idOf = (name: string) => project.graph.nodes.find((node) => node.displayName === name)!.id; const gateway = [...project.portForwardingRowsByNodeId.keys()][0]!; // The switch the web server behind the home router hangs on. const webServer = idOf('Webserver - 192.168.0.10'); const link = project.graph.links.find((each) => each.sourceNodeId === webServer || each.targetNodeId === webServer)!; const lanSwitch = link.sourceNodeId === webServer ? link.targetNodeId : link.sourceNodeId; setSoftware(doc, webServer, { install: ['commandPrompt'], remove: [] }, 'de', 'me'); // The web server's card with an address in small letters (a file may hold one so): the switch's table writes it in capitals. const webPort = (doc.getMap>('nodes').get(webServer)!.get('ports') as Y.Array>).get(0); webPort.set('mac', 'aa:bb:cc:00:11:22'); let now = 0; const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 }); const views = new Map(); const dropped: DroppedUnit[] = []; const resets: number[] = []; const messages: HostMessage[] = []; const errors: string[] = []; const connect = () => room.connect((message: HostMessage) => { messages.push(message); if (message.kind === 'view') views.set(`${message.node}/${message.app}`, message.view); if (message.kind === 'dropped') { if (message.reset) { resets.push(message.units.length); dropped.length = 0; } dropped.push(...message.units); } if (message.kind === 'error') errors.push(message.code); }); const client = connect(); const send = (fields: object, who = client) => room.receive(who, { v: 1, ...fields }); const run = (milliseconds: number) => { for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) { now += TICK_MILLISECONDS; room.tick(); } }; const terminal = (node: string, line: string) => send({ kind: 'input', node, app: 'terminal', action: 'enter', args: { line }, reqId: line }); return { room, idOf, gateway, lanSwitch, webServer, views, dropped, resets, messages, errors, connect, send, run, terminal }; } describe('the viewers in the room', () => { it("shows a switch's table and a home router's NAT table, and resets them", () => { const { gateway, lanSwitch, webServer, views, messages, errors, send, run, terminal } = setUp(); send({ kind: 'sub', topics: [`app:${lanSwitch}/sat`, `app:${gateway}/nat`, `app:${webServer}/sat`, `app:${lanSwitch}/nat`] }); send({ kind: 'start' }); run(TICK_MILLISECONDS); const sat = () => views.get(`${lanSwitch}/sat`) as SatView; const nat = () => views.get(`${gateway}/nat`) as NatView; // The port forwarding's entry, before anything went through. expect(nat()).toEqual({ rows: [{ cells: ['TCP', '80', '*', '192.168.0.10', '80'] }] }); expect(sat()).toEqual({ rows: [] }); // No switch, no home router: no table. expect([views.get(`${webServer}/sat`), views.get(`${lanSwitch}/nat`)]).toEqual([null, null]); terminal(webServer, 'ping 141.99.4.10'); run(10_000); expect(sat().rows.length).toBeGreaterThanOrEqual(2); expect(sat().rows.every((row) => Number.isInteger(row.port) && row.port >= 1 && row.updatedAtMilliseconds > 0)).toBe(true); expect(sat().rows.map((row) => row.macAddress)).toContain('AA:BB:CC:00:11:22'); expect(nat().rows.map((row) => row.cells.slice(0, 1).concat(row.cells.slice(2, 4)))).toEqual([ ['TCP', '*', '192.168.0.10'], ['ICMP', '141.99.4.10', '192.168.0.10'], ]); expect(nat().rows[1]!.updatedAtMilliseconds).toBeGreaterThan(0); send({ kind: 'input', node: lanSwitch, app: 'sat', action: 'reset', args: {}, reqId: 'sat' }); send({ kind: 'input', node: gateway, app: 'nat', action: 'reset', args: {}, reqId: 'nat' }); expect(sat()).toEqual({ rows: [] }); expect(nat()).toEqual({ rows: [{ cells: ['TCP', '80', '*', '192.168.0.10', '80'] }] }); // What the viewers never hand over. send({ kind: 'input', node: webServer, app: 'sat', action: 'reset', args: {}, reqId: 'a' }); send({ kind: 'input', node: lanSwitch, app: 'nat', action: 'reset', args: {}, reqId: 'b' }); send({ kind: 'input', node: lanSwitch, app: 'sat', action: 'sort', args: {}, reqId: 'c' }); expect(errors).toEqual(['unknownApp', 'unknownApp', 'unknownAction']); // The viewers have no lines: none are sent. expect(messages.filter((message) => message.kind === 'app')).toEqual([]); }); it('drops while any client holds "Pakete verwerfen", and lists what it dropped anew with each simulation', () => { const { webServer, dropped, resets, messages, errors, connect, send, run, terminal, room } = setUp(); send({ kind: 'sub', topics: ['dropped'] }); // Not while the design is shown. send({ kind: 'drop', on: true }); expect(errors).toEqual(['notRunning']); send({ kind: 'start' }); run(TICK_MILLISECONDS); terminal(webServer, 'ping 192.168.0.1'); run(12_000); expect(dropped).toEqual([]); // Nothing dropped, nothing sent after the subscription's (empty) list. expect(messages.filter((message) => message.kind === 'dropped')).toHaveLength(2); const other = connect(); send({ kind: 'drop', on: true }); send({ kind: 'drop', on: true }, other); send({ kind: 'drop', on: false }); // The other still holds it: the ping is dropped. terminal(webServer, 'ping 192.168.0.1'); run(12_000); expect(dropped.length).toBeGreaterThan(0); expect(dropped[0]!.text).toMatch(/^\[src=[0-9A-F:]+, dest=[0-9A-F:]+, type=0x8(00|06) \| \[/i); expect(dropped.map((unit) => unit.number)).toEqual(dropped.map((_, index) => index + 1)); const count = dropped.length; // Subscribed anew, the list comes whole. send({ kind: 'unsub', topics: ['dropped'] }); send({ kind: 'sub', topics: ['dropped'] }); expect([resets.at(-1), dropped.length]).toEqual([count, count]); // The other leaves: nothing is dropped any more. room.disconnect(other); terminal(webServer, 'ping 192.168.0.1'); run(12_000); expect(dropped.length).toBe(count); // A new simulation begins the list anew. send({ kind: 'stop' }); send({ kind: 'start' }); run(TICK_MILLISECONDS); expect(resets.at(-1)).toBe(0); expect(dropped).toEqual([]); expect(errors).toEqual(['notRunning']); }); it('forgets who held "Pakete verwerfen" when the simulation stops', () => { const { webServer, dropped, connect, send, run, terminal } = setUp(); const other = connect(); send({ kind: 'sub', topics: ['dropped'] }); send({ kind: 'start' }); run(TICK_MILLISECONDS); // Held through the stop: the letting go comes in the design, and is refused there. send({ kind: 'drop', on: true }, other); send({ kind: 'stop' }); send({ kind: 'drop', on: false }, other); send({ kind: 'start' }); run(TICK_MILLISECONDS); send({ kind: 'drop', on: true }); send({ kind: 'drop', on: false }); terminal(webServer, 'ping 192.168.0.1'); run(12_000); expect(dropped).toEqual([]); }); });