// The DNS server's window in the room, on FILIUS' example email_komplett: // its view (button, box, the three tables as /dns/hosts holds them), its // actions, /dns/hosts written by another into the document and shown at // once, and what a run leaves in the project (the file, whether it runs, // recursive resolution, the records in its settings). The server's steps // against Java's window are packages/core/test/dnsWindow.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 { changeFiles, fileText, readProject, writeProject } from '@mdg-filius/doc'; import { openProjectArchive } from '@mdg-filius/fls'; import type { DnsServerView, HostMessage } from '@mdg-filius/protocol'; import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js'; const here = dirname(fileURLToPath(import.meta.url)); const corpus = join(here, '../../../corpus/java-examples'); function setUp() { let ids = 0; const doc = new Y.Doc(); writeProject( doc, openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`), ); const id = readProject(doc).graph.nodes.find((node) => node.displayName === 'DNS-Server')!.id; let now = 0; const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 }); let view: DnsServerView | null | undefined; const errors: string[] = []; const lines: HostMessage[] = []; const client = room.connect((message: HostMessage) => { if (message.kind === 'app') lines.push(message); if (message.kind === 'view' && message.app === 'dnsServer') view = message.view as DnsServerView | null; if (message.kind === 'error') errors.push(message.code); }); const send = (fields: object) => room.receive(client, { v: 1, ...fields }); send({ kind: 'sub', topics: [`app:${id}/dnsServer`] }); send({ kind: 'start' }); const run = (milliseconds: number) => { for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) { now += TICK_MILLISECONDS; room.tick(); } }; run(TICK_MILLISECONDS); const act = (action: string, args: object = {}) => send({ kind: 'input', node: id, app: 'dnsServer', action, args, reqId: action }); return { doc, id, room, view: () => view, errors, lines, act, run, send }; } describe("the DNS server's window in the room", () => { it('shows the server and its tables, takes its actions, and shows what another writes into /dns/hosts at once', () => { const { doc, id, view, act, run, errors, lines } = setUp(); // Java's DNS window has no lines: none are sent. expect(lines).toEqual([]); const hosts = () => fileText(doc, id, '/dns/hosts')!.toString(); expect(view()!.active).toBe(true); const before = view()!.rows.A.length; act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' }); act('addRecord', { type: 'MX', name: 'schule.de', data: 'mail.schule.de' }); act('addRecord', { type: 'NS', name: 'schule.de', data: 'ns.schule.de' }); expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.1']); expect([view()!.rows.MX.at(-1), view()!.rows.NS.at(-1)]).toEqual([ ['schule.de.', 'mail.schule.de.'], ['schule.de.', 'ns.schule.de.'], ]); // In the document's file at once. expect(hosts().endsWith('neu.schule.de. A 3600 10.0.0.1\nschule.de. MX 3600 mail.schule.de.\nschule.de. NS 3600 ns.schule.de.\n')).toBe(true); act('changeRecord', { type: 'A', row: before, column: 1, value: '10.0.0.2' }); expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.2']); act('removeRecord', { type: 'NS', row: 0 }); expect(view()!.rows.NS).toEqual([]); act('setRecursive', { enabled: true }); act('setActive', { active: false }); expect([view()!.active, view()!.recursive]).toEqual([false, true]); expect(errors).toEqual([]); // The text editor writes the file: the table shows it. changeFiles(doc, id, 'me', (files) => files.writeTextFile('/dns/hosts', `${files.textFile('/dns/hosts')}extern.schule.de. A 3600 10.0.0.9\n`)); run(TICK_MILLISECONDS); expect(view()!.rows.A.at(-1)).toEqual(['extern.schule.de.', '10.0.0.9']); }); it('leaves in the project what the run made of the server', () => { const { doc, id, act, send } = setUp(); act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' }); act('setRecursive', { enabled: true }); act('setActive', { active: false }); send({ kind: 'stop' }); const project = readProject(doc); expect(project.activePrograms.get(id)?.has('dnsServer') ?? false).toBe(false); expect(project.dnsServerConfigurations.get(id)!.recursiveResolutionEnabled).toBe(true); expect(project.dnsServerConfigurations.get(id)!.records.at(-1)).toEqual({ name: 'neu.schule.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' }); expect(project.virtualFileSystems.get(id)!.textFile('/dns/hosts').endsWith('neu.schule.de. A 3600 10.0.0.1\n')).toBe(true); }); it('refuses what the window never hands over', () => { const { act, errors } = setUp(); act('setActive', { active: 'no' }); act('setRecursive', {}); act('addRecord', { type: 'TXT', name: 'a.de', data: 'x' }); act('addRecord', { type: 'A', name: '', data: '10.0.0.1' }); act('addRecord', { type: 'A', name: 'a.de', data: 5 }); act('removeRecord', { type: 'A', row: 1.5 }); act('removeRecord', { type: 'NS', row: 0 }); act('changeRecord', { type: 'A', row: 0, column: 2, value: 'x' }); act('changeRecord', { type: 'A', row: 0, column: 1, value: 'kein' }); act('changeRecord', { type: 'A', row: 0, column: 1, value: 3 }); act('restart'); expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction']); }); });