Files
mdg-filius/packages/fls/test/topology.test.ts
T
julian.kiedaischandClaude Opus 5.5 ccec734143 feat(fls): write the installed applications of a .fls, loaded by Java as the original
topologyWriter.ts puts each installed application of a PC or notebook under
its key and gives it the system software, as installApp does, with its
settings: a server's switch and port, a client's target, DNS recursion,
virtual hosts, the peer count, the personal firewall. The applications
follow the files, as in Java's own files: the web server, the peer-to-peer
application and the e-mail server make their directories only where the
files lack them, and the DNS server reads /dns/hosts when it is installed.
E-mail accounts, messages and address books are the next step.

The Java oracle now reports a personal firewall's rules in full, not their
number (the corpus has none; its result is byte-equal).

Checked by Java against Java: every corpus file, read and written again,
loads with the same applications as the original. The synthetic topology
adds every FILIUS application with settings away from its defaults, a
personal firewall with a rule, an own index.html (Java makes no second) and
a missing /peer2peer directory, which Java makes at the end. 12 mutation
probes, among them files and applications in reverse order, each loaded by
Java, were all caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 08:36:00 +00:00

588 lines
29 KiB
TypeScript

// The topology of every corpus file must be what Java FILIUS loads
// (corpus.java.json, tools/java-oracle): each device with class, name,
// position and interfaces, each cable with both ends, each documentation
// item — field by field. Then the rules the corpus does not exercise.
import { createHash } from 'node:crypto';
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
decodeJavaBeans,
readFlsArchive,
readTopology,
type FlsApplication,
type FlsDhcpServer,
type FlsFileTree,
type FlsModem,
type FlsNodeClass,
type FlsTopology,
type JavaEmailAccount,
} from '../src/index.js';
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
const javaView = JSON.parse(readFileSync(join(corpusRoot, 'corpus.java.json'), 'utf8')) as Record<string, unknown>;
function flsFiles(directory: string): string[] {
return readdirSync(directory)
.flatMap((name) => {
const path = join(directory, name);
return statSync(path).isDirectory() ? flsFiles(path) : name.endsWith('.fls') ? [path] : [];
})
.sort();
}
/**
* The palette label Java shows: not read from the file but set from the
* class while loading (setTyp(holeHardwareTyp())), in German.
*/
const TYPE_LABELS: Record<FlsNodeClass, string> = {
Rechner: 'Rechner',
Notebook: 'Notebook',
Switch: 'Switch / WLAN',
Vermittlungsrechner: 'Vermittlungsrechner',
Gateway: 'Heimrouter',
Modem: 'Modem',
};
/** FlsOracle.fileTree's shape: contents as length and SHA-256 of their UTF-8 bytes. */
function javaFileTree(tree: FlsFileTree): unknown {
const children = tree.children.map(javaFileTree);
switch (tree.kind) {
case 'directory':
return { directory: tree.name, children };
case 'file':
return {
file: tree.name,
type: tree.type,
length: tree.content?.length ?? null,
sha256: tree.content === null ? null : createHash('sha256').update(tree.content, 'utf8').digest('hex'),
children,
};
case 'other':
return { userObject: tree.className, children };
}
}
/** FlsOracle.applications' shape: Java's property names, sorted by key. */
function javaApplications(applications: FlsApplication[]): unknown[] {
return [...applications]
.sort((lhs, rhs) => ((lhs.key ?? '') < (rhs.key ?? '') ? -1 : 1))
.map((application) => ({
key: application.key,
class: application.javaClass,
...(application.isActive === undefined ? {} : { aktiv: application.isActive, port: application.port }),
...(application.targetPort === undefined
? {}
: { zielIPAdresse: application.targetAddress, zielPort: application.targetPort }),
...(application.recursiveResolutionEnabled === undefined
? {}
: { recursiveResolutionEnabled: application.recursiveResolutionEnabled }),
...(application.useVirtualHosts === undefined ? {} : { useVHost: application.useVirtualHosts }),
...(application.maxPeers === undefined ? {} : { maxTeilnehmerZahl: application.maxPeers }),
...(application.firewall === undefined
? {}
: {
activated: application.firewall.activated,
defaultPolicy: application.firewall.defaultPolicy,
dropICMP: application.firewall.dropICMP,
filterSYNSegmentsOnly: application.firewall.filterSYNSegmentsOnly,
filterUdp: application.firewall.filterUdp,
rules: application.firewall.rules.map((rule) => [
rule.srcIP,
rule.srcMask,
rule.destIP,
rule.destMask,
rule.port,
rule.protocol,
rule.action,
]),
}),
...(application.mailDomain === undefined
? {}
: {
mailDomain: application.mailDomain,
beanAccounts: application.beanAccounts!.map(javaEmailAccount),
fileAccounts: javaFileAccounts(application.fileAccounts!),
}),
...(application.received === undefined
? {}
: {
received: application.received.map(javaEmail),
sent: application.sent!.map(javaEmail),
drafts: application.drafts!.map(javaEmail),
addressBook: application.addressBook!.map((contact) => [
contact.name,
contact.firstName,
contact.street,
contact.houseNumber,
contact.postalCode,
contact.city,
contact.email,
contact.phone,
]),
fileAccount: application.fileAccount === null ? null : javaEmailAccount(application.fileAccount!),
}),
}));
}
function javaFileAccounts(accounts: JavaEmailAccount[] | null): unknown {
return accounts === null ? null : accounts.map(javaEmailAccount);
}
/** Java's property names, as FlsOracle.java writes them. */
function javaDhcpServer(server: FlsDhcpServer) {
return {
aktiv: server.isActive,
untergrenze: server.lowerBound,
obergrenze: server.upperBound,
gatewayip: server.gateway,
dnsserverip: server.dnsServer,
ownSettings: server.useOwnSettings,
staticAssignedAddresses: server.staticAssignedAddresses.map((entry) => `${entry.mac} ${entry.ip}`),
};
}
function javaModem(modem: FlsModem) {
return { mode: modem.mode, port: modem.port, ipAdresse: modem.ipAddress };
}
/** The topology in the shape FlsOracle.java writes, less what Swing computes (label width and height). */
function asJavaView(topology: FlsTopology, javaBounds: (number[] | null)[]) {
return {
success: true,
nodes: topology.nodes.map((node, index) => ({
class: node.javaClass,
name: node.name,
label: TYPE_LABELS[node.javaClass],
bounds: [node.x, node.y, ...(javaBounds[index]?.slice(2) ?? [])],
...(node.javaClass === 'Rechner' || node.javaClass === 'Notebook'
? { useIpAsName: node.useIpAsName, useMacAsName: node.useMacAsName }
: {}),
ports:
node.javaClass === 'Switch' || node.javaClass === 'Modem'
? Array.from({ length: node.portCount }, () => ({}))
: node.interfaces.map((nic) => ({ ...nic, mac: nic.mac ?? null })),
system: {
...node.system,
...('ssid' in node.system ? { ssid: node.system.ssid ?? null } : {}),
...(node.system.dhcpServer === undefined
? {}
: {
dhcpServer: javaDhcpServer(node.system.dhcpServer),
}),
...(node.system.modem === undefined ? {} : { modem: javaModem(node.system.modem) }),
...(node.system.fileSystem === undefined ? {} : { fileSystem: javaFileTree(node.system.fileSystem) }),
...('templateFiles' in node.system ? { templateFiles: undefined } : {}),
...(node.system.applications === undefined ? {} : { applications: javaApplications(node.system.applications) }),
...(node.system.firewall === undefined
? {}
: {
firewall: {
...node.system.firewall,
rules: node.system.firewall.rules.map((rule) => [
rule.srcIP,
rule.srcMask,
rule.destIP,
rule.destMask,
rule.port,
rule.protocol,
rule.action,
]),
},
}),
},
})),
cables: topology.links.map((link) => ({
ends: link.ends.map((end) => [end.node, end.port]),
wireless: link.wireless,
panel: link.panel,
})),
documentation: topology.documentation.map((item) => ({
type: item.type,
text: item.text,
bounds: [item.x, item.y, item.width, item.height],
color: item.color,
font: item.font === null ? null : [item.font.name, item.font.size, item.font.style],
})),
};
}
type JavaTree = { children: unknown[] };
/**
* The files a router's or home router's constructor makes, as Java loaded
* each one: they come first in its root, what the file adds follows. Each
* router's are cut off here and collected, and must be the same for every
* router of a class across the corpus.
*/
const templateChildren = new Map<string, Set<string>>();
function withoutTemplateFiles(view: unknown, topology: FlsTopology): unknown {
const { nodes, ...rest } = view as { nodes: { class: string; system: Record<string, unknown> }[] };
return {
...rest,
nodes: nodes.map((node, index) => {
const ours = topology.nodes[index]?.system;
if ((node.class !== 'Vermittlungsrechner' && node.class !== 'Gateway') || ours?.templateFiles !== true) return node;
const tree = node.system.fileSystem as JavaTree;
const added = ours.fileSystem?.children.length ?? 0;
const template = tree.children.slice(0, tree.children.length - added);
const seen = templateChildren.get(node.class) ?? new Set<string>();
seen.add(JSON.stringify(template));
templateChildren.set(node.class, seen);
return {
...node,
system: { ...node.system, fileSystem: { ...tree, children: tree.children.slice(tree.children.length - added) } },
};
}),
};
}
describe('corpus: the topology as Java loads it', () => {
for (const file of flsFiles(corpusRoot)) {
const name = relative(corpusRoot, file);
it(name, () => {
const values = decodeJavaBeans(readFlsArchive(new Uint8Array(readFileSync(file))).configurationXml);
const topology = readTopology(values);
const expected = javaView[name] as { nodes: { bounds: number[] | null }[] };
expect(asJavaView(topology, expected.nodes.map((node) => node.bounds))).toEqual(withoutTemplateFiles(expected, topology));
expect(topology.warnings).toEqual(EXPECTED_WARNINGS[name] ?? []);
});
}
it("a router's constructor makes the same files in every corpus file", () => {
for (const javaClass of ['Vermittlungsrechner', 'Gateway']) {
const templates = [...(templateChildren.get(javaClass) ?? [])].map((text) => JSON.parse(text) as { directory?: string }[]);
expect(templates, javaClass).toHaveLength(1);
expect(templates[0]!.map((child) => child.directory), javaClass).toEqual(['webserver']);
}
});
});
// ── Rules the corpus does not exercise ─────────────────────────────────────
function doc(body: string): string {
return `<?xml version="1.0" encoding="UTF-8"?>\n<java version="17" class="java.beans.XMLDecoder">\n${body}\n</java>\n`;
}
function topologyOf(body: string): FlsTopology {
return readTopology(decodeJavaBeans(doc(body)));
}
function nodeItem(device: string, extra = ''): string {
return `<void method="add"><object class="filius.gui.netzwerksicht.GUIKnotenItem">
<void property="knoten">${device}</void>${extra}</object></void>`;
}
describe('readTopology rules', () => {
it('passes addresses through Java setIp: canonical, null when invalid, unchanged when null', () => {
const { nodes } = topologyOf(`<string>Filius version: 2.9.4 (1.1.2026)</string>
<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner">
<void property="netzwerkInterfaces">
<void index="0"><void property="ip"><string>010.0..0.1</string></void>
<void property="subnetzMaske"><string>255.255.255.256</string></void></void>
<void method="add"><object class="filius.hardware.NetzwerkInterface">
<void property="ip"><null/></void><void property="mac"><string>0a:0b:0c:0d:0e:0f</string></void>
</object></void>
</void></object>`)}</object>`);
expect(nodes[0]!.interfaces).toEqual([
{ ip: '10.0.0.1', mask: null, mac: undefined, gateway: '', dns: '' },
{ ip: '192.168.0.10', mask: '255.255.255.0', mac: '0A:0B:0C:0D:0E:0F', gateway: '', dns: '' },
]);
});
it("gives only a Gateway's own first interface the WAN address, not one the file creates", () => {
const own = topologyOf(`<object class="java.util.LinkedList">${nodeItem(
'<object class="filius.hardware.knoten.Gateway"/>',
)}</object>`);
const replaced = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Gateway">
<void property="netzwerkInterfaces"><object class="java.util.LinkedList">
<void method="add"><object class="filius.hardware.NetzwerkInterface"/></void>
</object></void></object>`)}</object>`);
expect(own.nodes[0]!.interfaces.map((nic) => [nic.ip, nic.mask])).toEqual([
['42.0.0.10', '255.0.0.0'],
['192.168.0.10', '255.255.255.0'],
]);
expect(own.nodes[0]!.name).toBe('Heimrouter');
expect(replaced.nodes[0]!.interfaces.map((nic) => nic.ip)).toEqual(['192.168.0.10']);
});
it('places a device without bounds at 0,0 instead of leaving it out', () => {
const { nodes, warnings } = topologyOf(
`<object class="java.util.LinkedList">${nodeItem('<object class="filius.hardware.knoten.Notebook"/>')}</object>`,
);
expect(nodes.map((node) => [node.javaClass, node.x, node.y, node.portCount])).toEqual([['Notebook', 0, 0, 1]]);
expect(warnings).toEqual([]);
});
it('leaves out an unknown device and a cable to a port no device owns, and says so', () => {
const { nodes, links, warnings } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Drucker"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Switch"><void property="anschluesse">
<void id="Port0" index="0"/><void id="Port1" index="1"/></void></object>`)}
</object>
<object class="java.util.LinkedList">
<void method="add"><object class="filius.gui.netzwerksicht.GUIKabelItem"><void property="dasKabel">
<object class="filius.hardware.Kabel"><object idref="Port0"/><object idref="Port1"/></object></void></object></void>
<void method="add"><object class="filius.gui.netzwerksicht.GUIKabelItem"><void property="dasKabel">
<object class="filius.hardware.Kabel"><object idref="Port0"/><object class="filius.hardware.Port"/></object></void></object></void>
</object>`);
expect(nodes.map((node) => node.javaClass)).toEqual(['Switch']);
expect(links).toEqual([{ ends: [{ node: 0, port: 0 }, { node: 0, port: 1 }], wireless: false, panel: [null, null] }]);
expect(warnings).toEqual([
'node 1: unknown device class filius.hardware.knoten.Drucker, left out',
'cable 2: an end is not a port of a device, left out',
]);
});
it('reads the lists by position and only when the first element fits, as netzwerkLaden does', () => {
// No version line (files before 1.3.0); an empty cable list still takes its place.
const topology = topologyOf(`<object class="java.util.LinkedList">${nodeItem(
'<object class="filius.hardware.knoten.Rechner"/>',
)}</object>
<object class="java.util.LinkedList"/>
<object class="java.util.LinkedList"><void method="add"><object class="filius.gui.netzwerksicht.GUIDocuItem">
<void property="type"><int>1</int></void></object></void></object>`);
const wrongFirst = topologyOf(`<object class="java.util.LinkedList">
<void method="add"><string>not a node</string></void>
${nodeItem('<object class="filius.hardware.knoten.Rechner"/>')}</object>`);
expect(topology.versionLine).toBeNull();
expect(topology.nodes).toHaveLength(1);
expect(topology.documentation.map((item) => item.type)).toEqual([1]);
expect(wrongFirst.nodes).toEqual([]);
});
it("reads a router's firewall wherever the file reaches it, with Java's defaults for what it leaves out", () => {
// Settings under the installed application and under the web server's
// plug-in belong to the one firewall the constructor made.
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
<void method="get"><string>filius.software.firewall.Firewall</string>
<void property="allowRelatedPackets"><boolean>false</boolean></void>
<void property="ruleset">
<void method="add"><object class="filius.software.firewall.FirewallRule" id="FirewallRule0">
<void class="filius.software.firewall.FirewallRule" method="getField"><string>srcIP</string>
<void method="set"><object idref="FirewallRule0"/><string>10.0.0.0</string></void></void>
</object></void>
<void method="add"><object class="filius.software.firewall.FirewallRule">
<string>1.1.1.1</string><string>255.255.255.255</string><string></string><string></string>
<int>53</int><short>17</short><short>1</short>
</object></void>
</void>
</void>
<void method="get"><string>filius.software.www.WebServer</string>
<void property="plugins"><void method="get"><string>konfig</string>
<void property="firewall"><void property="activated"><boolean>true</boolean></void></void>
</void></void>
</void>
</void></void></object>`)}</object>`);
expect(nodes[0]!.system.firewall).toEqual({
activated: true,
defaultPolicy: 0,
dropICMP: false,
filterSYNSegmentsOnly: false,
filterUdp: true,
rules: [
{ srcIP: '10.0.0.0', srcMask: '', destIP: '', destMask: '', port: -1, protocol: 6, action: 0 },
{ srcIP: '1.1.1.1', srcMask: '255.255.255.255', destIP: '', destMask: '', port: 53, protocol: 17, action: 1 },
],
});
});
it('splits static DHCP assignments at Java whitespace and leaves out only a broken one', () => {
// U+3000 separates, U+00A0 does not; the third entry has no IP. Java
// throws there and never reads the fourth — an error not copied.
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="DHCPServer">
<void property="staticAssignedAddresses"><object class="java.util.ArrayList">
<void method="add"><string>0A:00:00:00:00:01<char code="#3000"/>10.0.0.1<char code="#9"/>extra</string></void>
<void method="add"><string>0A:00:00:00:00:02 10.0.0.2<char code="#A0"/>x</string></void>
<void method="add"><string>0A:00:00:00:00:03</string></void>
<void method="add"><string>0A:00:00:00:00:04 10.0.0.4</string></void>
</object></void>
</void></void></object>`)}</object>`);
expect(nodes[0]!.system.dhcpServer).toEqual({
isActive: false,
lowerBound: '0.0.0.0',
upperBound: '0.0.0.0',
gateway: '0.0.0.0',
dnsServer: '0.0.0.0',
useOwnSettings: false,
staticAssignedAddresses: [
{ mac: '0A:00:00:00:00:01', ip: '10.0.0.1' },
{ mac: '0A:00:00:00:00:02', ip: '10.0.0.2\u00A0x' },
{ mac: '0A:00:00:00:00:04', ip: '10.0.0.4' },
],
});
expect(warnings).toEqual([
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP, left out',
]);
});
it('reads a file system from `root` or `arbeitsVerzeichnis`, with files by setters or constructor', () => {
const { nodes, warnings } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Notebook"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Rechner"><void property="systemSoftware"><void property="dateisystem">
<void property="root"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>/</string>
<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode">
<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode">
<object class="filius.software.system.Datei"><string>a.txt</string><string>text</string><string>hallo</string>
<void property="dateiTyp"><string>txt</string></void>
</object>
</object></void>
</object></void>
<void method="insert"><object class="javax.swing.tree.DefaultMutableTreeNode"/><int>0</int></void>
</object></void>
</void></void></object>`)}</object>`);
expect(nodes.map((node) => node.system.fileSystem)).toEqual([
{ kind: 'directory', name: 'root', children: [] },
{
kind: 'directory',
name: '/',
children: [
{
kind: 'directory',
name: null,
children: [{ kind: 'file', name: 'a.txt', type: 'txt', content: 'hallo', children: [] }],
},
],
},
]);
expect(warnings).toEqual(['node 2: file system: insert on a node is not read']);
});
it("reads what a router's file adds to the constructor's files, or a root of its own", () => {
const folder = (name: string) =>
`<void method="add"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>${name}</string></object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Vermittlungsrechner"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware"><void property="dateisystem">
<void property="arbeitsVerzeichnis">${folder('www.conf')}</void>
</void></void></object>`)}
${nodeItem(`<object class="filius.hardware.knoten.Vermittlungsrechner"><void property="systemSoftware"><void property="dateisystem">
<void property="arbeitsVerzeichnis"><object class="javax.swing.tree.DefaultMutableTreeNode"><string>root</string>${folder('own')}</object></void>
</void></void></object>`)}
${nodeItem(`<object class="filius.hardware.knoten.Gateway"><void property="systemSoftware">
<void property="dateisystem"><object class="filius.software.system.Dateisystem"/></void>
</void></object>`)}</object>`);
const directory = (name: string, children: FlsFileTree[] = []): FlsFileTree => ({ kind: 'directory', name, children });
expect(nodes.map((node) => [node.system.fileSystem, node.system.templateFiles])).toEqual([
[directory('root'), true],
[directory('root', [directory('www.conf')]), true],
[directory('root', [directory('own')]), false],
[directory('root'), false],
]);
});
it("gives installed applications Java's measured defaults for what the file leaves out", () => {
const put = (className: string, body = '') =>
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
${put('filius.software.www.WebServer')}
${put('filius.software.clientserver.ClientBaustein')}
${put('filius.software.dateiaustausch.PeerToPeerAnwendung')}
${put('filius.software.firewall.Firewall')}
${put('filius.software.dns.DNSServer', '<void property="aktiv"><boolean>true</boolean></void><void property="port"><int>5353</int></void>')}
${put('filius.software.lokal.Terminal')}
</void></void></object>`)}</object>`);
const byClass = new Map(nodes[0]!.system.applications!.map((application) => [application.javaClass, application]));
expect(byClass.get('filius.software.www.WebServer')).toMatchObject({ isActive: false, port: 80, useVirtualHosts: false });
expect(byClass.get('filius.software.clientserver.ClientBaustein')).toMatchObject({ targetAddress: null, targetPort: 55_555 });
// Java rolls it: no default.
expect(byClass.get('filius.software.dateiaustausch.PeerToPeerAnwendung')).toMatchObject({ maxPeers: null });
expect(byClass.get('filius.software.firewall.Firewall')?.firewall).toMatchObject({ activated: true, defaultPolicy: 0 });
expect(byClass.get('filius.software.dns.DNSServer')).toMatchObject({ isActive: true, port: 5353, recursiveResolutionEnabled: false });
expect(Object.keys(byClass.get('filius.software.lokal.Terminal')!).sort()).toEqual(['javaClass', 'key', 'source']);
});
it('reads the e-mail beans through their setters: null names, string senders, a checked domain', () => {
const put = (className: string, body = '') =>
`<void method="put"><string>${className}</string><object class="${className}">${body}</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
${put('filius.software.email.EmailServer', `
<void property="mailDomain"><string>a_b.de</string></void>
<void property="listeBenutzerkonten"><void method="add"><object class="filius.software.email.EmailKonto">
<void property="benutzername"><string>bob</string></void>
<void property="nachname"><null/></void>
<void property="nachrichten"><void method="add"><object class="filius.software.email.Email">
<void property="absender"><string> Alice &lt;alice@senior.de&gt; </string></void>
</object></void></void>
</object></void></void>`)}
</void></void></object>`)}</object>`);
const server = nodes[0]!.system.applications![0]!;
// setMailDomain keeps "filius.de" for a domain that is not a domain name.
expect(server).toMatchObject({ isActive: false, mailDomain: 'filius.de' });
expect(server.beanAccounts![0]).toMatchObject({ username: 'bob', lastName: '', firstName: '' });
expect(server.beanAccounts![0]!.messages[0]!.sender).toEqual({ name: 'Alice', mailAddress: 'alice@senior.de' });
});
it("reads an older file's kontoListe, which FILIUS 2.14 ignores, in the order of its entries", () => {
const legacyAccount = (username: string) =>
`<void method="put"><string>${username}@senior.de</string><object class="filius.software.email.EmailKonto">
<void property="benutzername"><string>${username}</string></void>
<void property="pop3port"><string>110</string></void>
</object></void>`;
const { nodes } = topologyOf(`<object class="java.util.LinkedList">${nodeItem(`<object class="filius.hardware.knoten.Rechner">
<void property="systemSoftware"><void property="installierteAnwendungen">
<void method="put"><string>filius.software.email.EmailAnwendung</string><object class="filius.software.email.EmailAnwendung">
<void property="kontoListe">${legacyAccount('rechner3')}${legacyAccount('rechner4')}</void>
</object></void>
</void></void></object>`)}</object>`);
const client = nodes[0]!.system.applications![0]!;
expect(client.fileAccount).toBeNull();
expect(client.legacyAccounts!.map((account) => [account.username, account.pop3Port])).toEqual([
['rechner3', '110'],
['rechner4', '110'],
]);
});
it("gives a modem Java's defaults and keeps an address the file sets to null", () => {
const { nodes } = topologyOf(`<object class="java.util.LinkedList">
${nodeItem('<object class="filius.hardware.knoten.Modem"/>')}
${nodeItem(`<object class="filius.hardware.knoten.Modem"><void property="systemSoftware">
<void property="ipAdresse"><null/></void><void property="mode"><int>1</int></void>
</void></object>`)}</object>`);
expect(nodes.map((node) => node.system.modem)).toEqual([
{ mode: 2, port: 12_345, ipAddress: 'localhost' },
{ mode: 1, port: 12_345, ipAddress: null },
]);
});
it("reads colors as Java's constructors do and keeps an invalid one unset", () => {
const colors = ['<int>16711935</int>', '<int>1</int><int>2</int><int>3</int>', '<int>1</int><int>2</int><int>300</int><int>4</int>'];
const { documentation, warnings } = topologyOf(`<object class="java.util.LinkedList"/><object class="java.util.LinkedList"/>
<object class="java.util.LinkedList">${colors
.map(
(args) => `<void method="add"><object class="filius.gui.netzwerksicht.GUIDocuItem">
<void property="color"><object class="java.awt.Color">${args}</object></void>
<void property="font"><object class="java.awt.Font"><string>Dialog</string><int>5</int><int>14</int></object></void>
</object></void>`,
)
.join('')}</object>`);
expect(documentation.map((item) => item.color)).toEqual([[255, 0, 255, 255], [1, 2, 3, 255], null]);
expect(documentation[0]!.font).toEqual({ name: 'Dialog', size: 14, style: 0 });
expect(warnings).toEqual(['documentation item: color component outside 0…255, left unset (as in Java)']);
});
});