fix(fls): follow Java's formats, not Java's errors
Java stays the reference for the .fls format, but its errors are no longer copied. The Java oracle still measures Java as it is; each deliberate departure is named in the tests with its reason (JAVA_BUGS) and must really depart. The corpus - what Java itself writes - still reads exactly as in Java. - /dns/hosts: a label may start with a digit, holds no backslash, and the last one is not all digits (RFC 1123, RFC 3696); A, MX and NS are read in any case (RFC 4343); a negative TTL is invalid, so 3600 (RFC 2181). Also for the e-mail server's domain. - /mailserver/konten.txt: empty fields count (Java's StringTokenizer skips them and shifts the rest, so an empty password breaks the account); a short line alone is left out, with a warning (Java throws for the whole file); a message keeps an empty text or subject (Java's split loses it); a ";" in an unescaped subject stays in the message; CRLF lines. - /konten.txt: the last complete line is the account, also with an empty address (Java takes the last line and loses the account); CRLF lines. - Static DHCP assignments: a broken entry alone is left out (Java loses every entry after it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
// Java's DNS server keeps its records in the text file /dns/hosts and reads
|
||||
// them on every query (DNSServer.leseRecordListe, ResourceRecord(String)).
|
||||
// Measured on test/dns-hosts.cases.txt against Java (dns-hosts.java.json)
|
||||
// and Swift (dns-hosts.swift.json); where the two differ, Java decides:
|
||||
// and Swift (dns-hosts.swift.json). Java's format decides where the two
|
||||
// differ:
|
||||
// - fields are split at single spaces only: a tab or NBSP joins two fields;
|
||||
// - there are no comments, no "IN" and no zone-file order;
|
||||
// - a TTL that is not a Java int silently becomes 3600;
|
||||
// - a name Java does not accept becomes the empty name, and the record
|
||||
// still counts - it just never answers, like any record whose type is
|
||||
// not exactly A, MX or NS (lower-case "a" included);
|
||||
// - a TTL that is not a number silently becomes 3600;
|
||||
// - a name that is not a domain name becomes the empty name, and the record
|
||||
// still counts - it just never answers, like one of another type;
|
||||
// - duplicates stay.
|
||||
// Java's errors are not copied (each named in dnsHosts.test.ts): a label may
|
||||
// start with a digit and holds no backslash (RFC 1123 - Java's pattern has
|
||||
// both wrong; see isDomainName), the type A, MX or NS is read in any case
|
||||
// (RFC 4343), and a negative TTL is invalid (RFC 2181).
|
||||
|
||||
import type { DnsResourceRecord } from '@mdg-filius/core';
|
||||
import { javaFileContent } from './javaFileSystem.js';
|
||||
@@ -16,8 +20,8 @@ import { javaIpCheck, javaParseInt } from './javaIp.js';
|
||||
import { javaTrim } from './javaString.js';
|
||||
import type { FlsFileTree } from './systemSoftware.js';
|
||||
|
||||
/** A record as Java's ResourceRecord holds it (the getters). */
|
||||
export interface JavaResourceRecord {
|
||||
/** A record of /dns/hosts, as Java's ResourceRecord holds it (the getters). */
|
||||
export interface HostsRecord {
|
||||
readonly domainName: string;
|
||||
readonly type: string;
|
||||
readonly ttl: number;
|
||||
@@ -26,10 +30,22 @@ export interface JavaResourceRecord {
|
||||
|
||||
export const DNS_HOSTS_PATH = '/dns/hosts';
|
||||
|
||||
// EingabenUeberpruefung.musterDomain, including its quirks: labels start with
|
||||
// a letter, labels after the first may hold a backslash, and the empty
|
||||
// string is a valid domain.
|
||||
export const JAVA_DOMAIN = /^([a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z]([a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])?)*)?\.?$/;
|
||||
// Labels of letters, digits and inner hyphens, an optional final dot; the
|
||||
// empty string counts too.
|
||||
const DOMAIN_LABELS = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)?\.?$/;
|
||||
/**
|
||||
* A domain name as EingabenUeberpruefung.musterDomain means it (the empty
|
||||
* string included). Java's pattern wants every label to start with a letter
|
||||
* and lets a backslash into the labels after the first; instead a label may
|
||||
* start with a digit (RFC 1123), but the last one must not be all digits
|
||||
* (RFC 3696), so that no IPv4 address passes for a name.
|
||||
*/
|
||||
export function isDomainName(text: string): boolean {
|
||||
if (!DOMAIN_LABELS.test(text)) return false;
|
||||
const labels = text.split('.').filter((label) => label !== '');
|
||||
return labels.length === 0 || !/^[0-9]+$/.test(labels[labels.length - 1]!);
|
||||
}
|
||||
|
||||
// EingabenUeberpruefung.musterIpAdresse.
|
||||
const JAVA_IP_ADDRESS =
|
||||
/^(0*([1-9]?[0-9]?|1[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.((0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))\.){2}(0*(1?[0-9]{1,2}|2[0-4]?[0-9]|25[0-5]))$/;
|
||||
@@ -39,31 +55,39 @@ export function javaHostsFileContent(root: FlsFileTree | undefined): string | un
|
||||
return javaFileContent(root, DNS_HOSTS_PATH);
|
||||
}
|
||||
|
||||
const ANSWERED_TYPES = new Set(['A', 'MX', 'NS']);
|
||||
|
||||
/** `DNSServer.leseRecordListe` on the content of /dns/hosts. */
|
||||
export function readJavaDnsHosts(content: string): JavaResourceRecord[] {
|
||||
const records: JavaResourceRecord[] = [];
|
||||
export function readDnsHosts(content: string): HostsRecord[] {
|
||||
const records: HostsRecord[] = [];
|
||||
for (const token of content.split('\n')) {
|
||||
const line = javaTrim(token);
|
||||
// line.split(" ", 5).length < 4: fewer than three single spaces.
|
||||
if (line === '' || line.split(' ').length < 4) continue;
|
||||
const record = javaResourceRecord(line);
|
||||
if (isValidJavaRecord(record)) records.push(record);
|
||||
const record = hostsRecord(line);
|
||||
if (isValidRecord(record)) records.push(record);
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
/** `new ResourceRecord(line)`: up to four space-separated fields. */
|
||||
function javaResourceRecord(line: string): JavaResourceRecord {
|
||||
function hostsRecord(line: string): HostsRecord {
|
||||
const fields = line.split(' ').filter((field) => field !== '').map(javaTrim);
|
||||
let domainName = '';
|
||||
let type = 'A';
|
||||
let ttl = 3600;
|
||||
let rdata = '';
|
||||
if (fields[0] !== undefined && JAVA_DOMAIN.test(fields[0])) {
|
||||
if (fields[0] !== undefined && isDomainName(fields[0])) {
|
||||
domainName = fields[0].endsWith('.') ? fields[0] : `${fields[0]}.`;
|
||||
}
|
||||
if (fields[1] !== undefined) type = fields[1];
|
||||
if (fields[2] !== undefined) ttl = javaParseInt(fields[2]) ?? ttl;
|
||||
if (fields[1] !== undefined) {
|
||||
const upper = fields[1].replace(/[a-z]/g, (letter) => letter.toUpperCase());
|
||||
type = ANSWERED_TYPES.has(upper) ? upper : fields[1];
|
||||
}
|
||||
if (fields[2] !== undefined) {
|
||||
const parsed = javaParseInt(fields[2]);
|
||||
if (parsed !== undefined && parsed >= 0) ttl = parsed;
|
||||
}
|
||||
if (fields[3] !== undefined) {
|
||||
// Java tests ".*\." (no line terminator before the dot); a target with
|
||||
// one fails the domain check anyway, so endsWith says the same.
|
||||
@@ -73,13 +97,13 @@ function javaResourceRecord(line: string): JavaResourceRecord {
|
||||
return { domainName, type, ttl, rdata };
|
||||
}
|
||||
|
||||
function isValidJavaRecord(record: JavaResourceRecord): boolean {
|
||||
function isValidRecord(record: HostsRecord): boolean {
|
||||
switch (record.type) {
|
||||
case 'A':
|
||||
return JAVA_DOMAIN.test(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata);
|
||||
return isDomainName(record.domainName) && JAVA_IP_ADDRESS.test(record.rdata);
|
||||
case 'MX':
|
||||
case 'NS':
|
||||
return JAVA_DOMAIN.test(record.domainName) && JAVA_DOMAIN.test(record.rdata);
|
||||
return isDomainName(record.domainName) && isDomainName(record.rdata);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
@@ -87,11 +111,11 @@ function isValidJavaRecord(record: JavaResourceRecord): boolean {
|
||||
|
||||
/**
|
||||
* The records Java answers with, in the model's form: Java looks them up by
|
||||
* type (exactly "A", "MX", "NS") and by name ignoring case, so a record with
|
||||
* the empty name or another type never answers and is left out. The file
|
||||
* itself stays as it is.
|
||||
* type (A, MX, NS) and by name ignoring case, so a record with the empty
|
||||
* name or another type never answers and is left out. The file itself stays
|
||||
* as it is.
|
||||
*/
|
||||
export function dnsRecordsOf(records: readonly JavaResourceRecord[], warn: (message: string) => void): DnsResourceRecord[] {
|
||||
export function dnsRecordsOf(records: readonly HostsRecord[], warn: (message: string) => void): DnsResourceRecord[] {
|
||||
const answered: DnsResourceRecord[] = [];
|
||||
for (const record of records) {
|
||||
if (record.domainName === '') continue;
|
||||
|
||||
@@ -91,7 +91,7 @@ import {
|
||||
type TopologyNode,
|
||||
type VirtualFileContent,
|
||||
} from '@mdg-filius/core';
|
||||
import { dnsRecordsOf, javaHostsFileContent, readJavaDnsHosts } from './dnsHosts.js';
|
||||
import { dnsRecordsOf, javaHostsFileContent, readDnsHosts } from './dnsHosts.js';
|
||||
import type { FlsApplicationClass, FlsFileTree, FlsFirewall } from './systemSoftware.js';
|
||||
import type { FlsDocumentationItem, FlsInterface, FlsNode, FlsNodeClass, FlsTopology } from './topology.js';
|
||||
|
||||
@@ -420,7 +420,7 @@ function registerPrograms(source: FlsNode, node: TopologyNode, system: SystemCon
|
||||
const primary = trimmedOrEmpty(source.interfaces[0]?.ip);
|
||||
const forwarding = recursive && forwarder !== '' && forwarder !== primary;
|
||||
const hosts = javaHostsFileContent(source.system.fileSystem);
|
||||
const records = dnsRecordsOf(readJavaDnsHosts(hosts ?? ''), (message) =>
|
||||
const records = dnsRecordsOf(readDnsHosts(hosts ?? ''), (message) =>
|
||||
warnings.push(`node '${node.displayName}': ${message}`),
|
||||
);
|
||||
system.dnsServerConfigurations.set(
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
// the bean list, and /konten.txt, the client's account, read (laden) when
|
||||
// its window opens. The client's account is in no bean: its accessors
|
||||
// (holeKontoListe, setzeKontoListe) are not bean properties.
|
||||
// Checked against Java on the corpus (corpus.java.json, applications).
|
||||
// Checked against Java on the corpus (corpus.java.json, applications) and on
|
||||
// the account files of test/email-*.cases.txt, where Java's errors in reading
|
||||
// them are not copied (see readEmailServerFile, readEmailClientFile).
|
||||
|
||||
import { isJavaObject, listItems, simpleClassName, type JavaObject, type JavaValue } from './javabeans.js';
|
||||
import { javaSplit, javaTokens, javaTrim } from './javaString.js';
|
||||
import { javaSplit, javaTrim } from './javaString.js';
|
||||
|
||||
/** AddressEntry: a display name, an address, or both. */
|
||||
export interface JavaAddressEntry {
|
||||
@@ -63,11 +65,6 @@ export interface JavaContact {
|
||||
readonly phone: string | null;
|
||||
}
|
||||
|
||||
/** An exception Java throws while reading, by its simple class name. */
|
||||
export interface JavaFailure {
|
||||
readonly exception: string;
|
||||
}
|
||||
|
||||
export const EMAIL_SERVER_FILE = '/mailserver/konten.txt';
|
||||
export const EMAIL_CLIENT_FILE = '/konten.txt';
|
||||
|
||||
@@ -213,17 +210,25 @@ function addressEntries(raw: string): JavaAddressEntry[] {
|
||||
|
||||
/**
|
||||
* `EmailServer.stringZuListeBenutzerkonten`: one account a line —
|
||||
* user;domain;password;last name;first name;messages — split by a
|
||||
* StringTokenizer, which skips empty fields; an account with fewer than five
|
||||
* fields makes Java throw. Messages are "#"-separated, seven "$"-separated
|
||||
* fields each: sender, to, cc, bcc, date, subject, text. Java's split drops
|
||||
* trailing empty fields, so a message with an empty text is lost.
|
||||
* user;domain;password;last name;first name;messages. Messages are
|
||||
* "#"-separated, seven "$"-separated fields each: sender, to, cc, bcc, date,
|
||||
* subject, text. Java's errors are not copied (each named in
|
||||
* javaEmail.test.ts): its StringTokenizer skips empty fields, so an empty
|
||||
* password shifts the rest; one line with fewer than five fields makes it
|
||||
* throw for the whole file; its split drops trailing empty fields, so a
|
||||
* message with an empty text or subject is lost; a CR of a CRLF line stays.
|
||||
* Here fields count also when empty, a short line alone is left out, and a
|
||||
* message needs its seven fields, empty or not.
|
||||
*/
|
||||
export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | JavaFailure {
|
||||
export function readEmailServerFile(content: string, warn: Warn): JavaEmailAccount[] {
|
||||
const accounts: JavaEmailAccount[] = [];
|
||||
for (const line of javaTokens(content, '\n')) {
|
||||
const fields = javaTokens(line, ';');
|
||||
if (fields.length < 5) return { exception: 'NoSuchElementException' };
|
||||
lines(content).forEach((line, index) => {
|
||||
if (line === '') return;
|
||||
const fields = splitAtMost(line, ';', 6);
|
||||
if (fields.length < 5) {
|
||||
warn(`${EMAIL_SERVER_FILE}: line ${index + 1} has fewer than five fields, left out`);
|
||||
return;
|
||||
}
|
||||
const [username, , password, lastName, firstName, messages] = fields as [string, string, string, string, string, string?];
|
||||
accounts.push({
|
||||
username,
|
||||
@@ -235,16 +240,20 @@ export function readJavaEmailServerFile(content: string): JavaEmailAccount[] | J
|
||||
pop3Port: null,
|
||||
smtpServer: null,
|
||||
smtpPort: null,
|
||||
messages: messages === undefined ? [] : fileMessages(messages),
|
||||
messages: messages === undefined ? [] : fileMessages(messages, username, warn),
|
||||
});
|
||||
}
|
||||
});
|
||||
return accounts;
|
||||
}
|
||||
|
||||
function fileMessages(messages: string): JavaEmail[] {
|
||||
return javaSplit(messages, '#').flatMap((message) => {
|
||||
const fields = javaSplit(message, '$');
|
||||
if (fields.length !== 7) return [];
|
||||
function fileMessages(messages: string, username: string, warn: Warn): JavaEmail[] {
|
||||
return messages.split('#').flatMap((message) => {
|
||||
if (message === '') return [];
|
||||
const fields = message.split('$');
|
||||
if (fields.length !== 7) {
|
||||
warn(`${EMAIL_SERVER_FILE}: a message of '${username}' has ${fields.length} fields, not seven, left out`);
|
||||
return [];
|
||||
}
|
||||
const [sender, to, cc, bcc, dateReceived, subject, body] = fields as [string, string, string, string, string, string, string];
|
||||
return [
|
||||
{
|
||||
@@ -263,6 +272,17 @@ function fileMessages(messages: string): JavaEmail[] {
|
||||
});
|
||||
}
|
||||
|
||||
/** The lines of a text file, LF or CRLF. */
|
||||
function lines(content: string): string[] {
|
||||
return content.split('\n').map((line) => (line.endsWith('\r') ? line.slice(0, -1) : line));
|
||||
}
|
||||
|
||||
/** Split at a separator into at most `limit` parts, empty ones included; the last keeps the rest. */
|
||||
function splitAtMost(text: string, separator: string, limit: number): string[] {
|
||||
const parts = text.split(separator);
|
||||
return parts.length <= limit ? parts : [...parts.slice(0, limit - 1), parts.slice(limit - 1).join(separator)];
|
||||
}
|
||||
|
||||
/** brtonl, then undoSpecialChar: "|" is a line break, "&&00124&&" a "|". */
|
||||
function unescapedBody(body: string): string {
|
||||
return body
|
||||
@@ -274,31 +294,36 @@ function unescapedBody(body: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* `EmailAnwendung.laden`: every line makes an account, the last one stays.
|
||||
* `new EmailKonto(line)` takes nine ";"-separated fields — POP3 server, SMTP
|
||||
* server, POP3 port, SMTP port, user, password, last name, first name,
|
||||
* address — and with fewer keeps none (trailing empty fields do not count).
|
||||
* Null when the file has no line at all.
|
||||
* `EmailAnwendung.laden`: an account of nine ";"-separated fields — POP3
|
||||
* server, SMTP server, POP3 port, SMTP port, user, password, last name,
|
||||
* first name, address; further fields are ignored. Java takes the last line
|
||||
* whatever it holds; its errors are not copied (each named in
|
||||
* javaEmail.test.ts): a last line with fewer fields — also a complete one
|
||||
* whose address is empty, since split drops trailing empty fields — leaves
|
||||
* the account empty, and a CR of a CRLF line stays in the address. Here the
|
||||
* last line with all nine fields is the account. Null when there is none.
|
||||
*/
|
||||
export function readJavaEmailClientFile(content: string): JavaEmailAccount | null {
|
||||
const lines = javaSplit(content, '\n');
|
||||
const line = lines[lines.length - 1];
|
||||
if (line === undefined) return null;
|
||||
const fields = javaSplit(line, ';');
|
||||
const empty = { username: null, password: null, lastName: '', firstName: '', emailAddress: null };
|
||||
const servers = { pop3Server: null, pop3Port: null, smtpServer: null, smtpPort: null };
|
||||
if (fields.length <= 8) return { ...empty, ...servers, messages: [] };
|
||||
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
|
||||
return {
|
||||
username: username!,
|
||||
password: password!,
|
||||
lastName: lastName!,
|
||||
firstName: firstName!,
|
||||
emailAddress: emailAddress!,
|
||||
pop3Server: pop3Server!,
|
||||
pop3Port: pop3Port!,
|
||||
smtpServer: smtpServer!,
|
||||
smtpPort: smtpPort!,
|
||||
messages: [],
|
||||
};
|
||||
export function readEmailClientFile(content: string, warn: Warn): JavaEmailAccount | null {
|
||||
let account: JavaEmailAccount | null = null;
|
||||
lines(content).forEach((line, index) => {
|
||||
const fields = line.split(';');
|
||||
if (fields.length < 9) {
|
||||
if (javaTrim(line) !== '') warn(`${EMAIL_CLIENT_FILE}: line ${index + 1} is no account of nine fields, ignored`);
|
||||
return;
|
||||
}
|
||||
const [pop3Server, smtpServer, pop3Port, smtpPort, username, password, lastName, firstName, emailAddress] = fields as string[];
|
||||
account = {
|
||||
username: username!,
|
||||
password: password!,
|
||||
lastName: lastName!,
|
||||
firstName: firstName!,
|
||||
emailAddress: emailAddress!,
|
||||
pop3Server: pop3Server!,
|
||||
pop3Port: pop3Port!,
|
||||
smtpServer: smtpServer!,
|
||||
smtpPort: smtpPort!,
|
||||
messages: [],
|
||||
};
|
||||
});
|
||||
return account;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// java.lang.String and java.util.StringTokenizer as FILIUS's text formats
|
||||
// use them — each differs from its JavaScript namesake.
|
||||
// java.lang.String as FILIUS's text formats use it - each differs from its
|
||||
// JavaScript namesake.
|
||||
|
||||
/** `String.trim()`: every UTF-16 unit up to U+0020 off both ends. */
|
||||
export function javaTrim(text: string): string {
|
||||
@@ -17,8 +17,3 @@ export function javaSplit(text: string, separator: string): string[] {
|
||||
while (parts.length > 0 && parts[parts.length - 1] === '') parts.pop();
|
||||
return parts;
|
||||
}
|
||||
|
||||
/** `new StringTokenizer(text, separator)`: the non-empty runs between separators. */
|
||||
export function javaTokens(text: string, separator: string): string[] {
|
||||
return text.split(separator).filter((token) => token !== '');
|
||||
}
|
||||
|
||||
@@ -27,19 +27,18 @@
|
||||
// not know Java's constructors and sees a placeholder per path; they are
|
||||
// merged here.
|
||||
|
||||
import { JAVA_DOMAIN } from './dnsHosts.js';
|
||||
import { isDomainName } from './dnsHosts.js';
|
||||
import {
|
||||
EMAIL_CLIENT_FILE,
|
||||
EMAIL_SERVER_FILE,
|
||||
javaContacts,
|
||||
javaEmailAccounts,
|
||||
javaEmails,
|
||||
readJavaEmailClientFile,
|
||||
readJavaEmailServerFile,
|
||||
readEmailClientFile,
|
||||
readEmailServerFile,
|
||||
type JavaContact,
|
||||
type JavaEmail,
|
||||
type JavaEmailAccount,
|
||||
type JavaFailure,
|
||||
} from './javaEmail.js';
|
||||
import { javaFileContent } from './javaFileSystem.js';
|
||||
import {
|
||||
@@ -151,16 +150,16 @@ export interface FlsApplication {
|
||||
maxPeers?: number | null;
|
||||
/** Firewall: the personal firewall. */
|
||||
firewall?: FlsFirewall;
|
||||
/** EmailServer: its domain (Java keeps "filius.de" for one its pattern rejects). */
|
||||
/** EmailServer: its domain ("filius.de" for one that is not a domain name, as Java's setter keeps it). */
|
||||
mailDomain?: string;
|
||||
/** EmailServer: the accounts of the bean list. */
|
||||
beanAccounts?: JavaEmailAccount[];
|
||||
/**
|
||||
* EmailServer: the accounts of /mailserver/konten.txt, which Java reads at
|
||||
* every simulation start — null without the file (Java then starts with
|
||||
* none), a failure where Java throws.
|
||||
* none).
|
||||
*/
|
||||
fileAccounts?: JavaEmailAccount[] | JavaFailure | null;
|
||||
fileAccounts?: JavaEmailAccount[] | null;
|
||||
/** EmailAnwendung: its folders (Java's empfangene, gesendete, erstellte Nachrichten). */
|
||||
received?: JavaEmail[];
|
||||
sent?: JavaEmail[];
|
||||
@@ -313,10 +312,10 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
|
||||
case 'filius.software.email.EmailServer': {
|
||||
entry.isActive = boolean(at('aktiv'), false, 'EmailServer aktiv', warn);
|
||||
const domain = optionalString(at('mailDomain'), 'EmailServer mailDomain', warn);
|
||||
entry.mailDomain = typeof domain === 'string' && JAVA_DOMAIN.test(domain) ? domain : 'filius.de';
|
||||
entry.mailDomain = typeof domain === 'string' && isDomainName(domain) ? domain : 'filius.de';
|
||||
entry.beanAccounts = javaEmailAccounts(at('listeBenutzerkonten'), warn);
|
||||
const file = javaFileContent(root, EMAIL_SERVER_FILE);
|
||||
entry.fileAccounts = file === undefined ? null : readJavaEmailServerFile(file);
|
||||
entry.fileAccounts = file === undefined ? null : readEmailServerFile(file, warn);
|
||||
break;
|
||||
}
|
||||
case 'filius.software.email.EmailAnwendung': {
|
||||
@@ -325,7 +324,7 @@ function applications(map: JavaValue | undefined, root: FlsFileTree, warn: Warn)
|
||||
entry.drafts = javaEmails(at('erstellteNachrichten'), warn);
|
||||
entry.addressBook = javaContacts(at('adressbuch'), warn);
|
||||
const file = javaFileContent(root, EMAIL_CLIENT_FILE);
|
||||
entry.fileAccount = file === undefined ? null : readJavaEmailClientFile(file);
|
||||
entry.fileAccount = file === undefined ? null : readEmailClientFile(file, warn);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -406,16 +405,17 @@ function dhcpServer(server: JavaValue | undefined, warn: Warn): FlsDhcpServer {
|
||||
|
||||
/**
|
||||
* setStaticAssignedAddresses: each entry split at whitespace (StringUtils.split),
|
||||
* MAC and IP from the first two parts. An entry with fewer throws inside the
|
||||
* loop — XMLDecoder swallows it, and the entries after it are lost.
|
||||
* MAC and IP from the first two parts. An entry with fewer makes Java throw
|
||||
* inside the loop — XMLDecoder swallows it, and the entries after it are
|
||||
* lost. That error is not copied: such an entry alone is left out.
|
||||
*/
|
||||
function staticAssignments(list: JavaValue | undefined, warn: Warn): { mac: string; ip: string }[] {
|
||||
const result: { mac: string; ip: string }[] = [];
|
||||
for (const entry of listItems(list)) {
|
||||
const parts = typeof entry === 'string' ? javaWhitespaceSplit(entry) : [];
|
||||
if (parts.length < 2) {
|
||||
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP; Java stops reading here`);
|
||||
break;
|
||||
warn(`DHCPServer: static assignment ${describe(entry)} has no MAC and IP, left out`);
|
||||
continue;
|
||||
}
|
||||
result.push({ mac: parts[0]!, ip: parts[1]! });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// Java's reading of /dns/hosts (DNSServer.leseRecordListe): every case in
|
||||
// dns-hosts.cases.txt was run through Java (dns-hosts.java.json) and through
|
||||
// Swift's hosts-file codec (dns-hosts.swift.json, tools/swift-oracle). The
|
||||
// Java view must match Java exactly; the records the model keeps must match
|
||||
// Swift except where Java answers differently — each case named below.
|
||||
// reader must match Java except where Java errs (JAVA_BUGS); the records the
|
||||
// model keeps must match Swift except where Java's format says otherwise —
|
||||
// each case named below.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { dnsRecordsOf, readJavaDnsHosts } from '../src/dnsHosts.js';
|
||||
import { dnsRecordsOf, readDnsHosts } from '../src/dnsHosts.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
type Row = [string, string, number, string];
|
||||
@@ -21,7 +22,7 @@ function unescape(line: string): string {
|
||||
}
|
||||
|
||||
function modelRows(line: string, warnings: string[] = []): Row[] {
|
||||
return dnsRecordsOf(readJavaDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [
|
||||
return dnsRecordsOf(readDnsHosts(unescape(line)), (message) => warnings.push(message)).map((record) => [
|
||||
record.name,
|
||||
record.type,
|
||||
record.ttlSeconds,
|
||||
@@ -31,13 +32,6 @@ function modelRows(line: string, warnings: string[] = []): Row[] {
|
||||
|
||||
/** Lines Swift reads as records that Java never answers. */
|
||||
const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
|
||||
{
|
||||
reason: 'a type in lower case is another type in Java, which never answers',
|
||||
lines: [
|
||||
String.raw`www.filius.de a 3600 1.2.3.4\u000A`,
|
||||
String.raw`filius.de mx 3600 mail.filius.de\u000A`,
|
||||
],
|
||||
},
|
||||
{
|
||||
reason: 'Java splits fields at single spaces only: a tab or NBSP is part of a field',
|
||||
lines: [
|
||||
@@ -65,12 +59,8 @@ const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
reason: 'a label that starts with a digit, or two final dots, make the name empty in Java',
|
||||
lines: [
|
||||
String.raw`1und1.de A 3600 1.2.3.4\u000A`,
|
||||
String.raw`www.1und1.de A 3600 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`,
|
||||
],
|
||||
reason: 'two final dots make the name empty in Java',
|
||||
lines: [String.raw`www.filius.de.. A 3600 1.2.3.4\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'an MX or NS target must be a name Java accepts: no preference, no address, one final dot',
|
||||
@@ -85,27 +75,19 @@ const SWIFT_READS_JAVA_NEVER_ANSWERS: { reason: string; lines: string[] }[] = [
|
||||
/** Lines Java answers that Swift drops or reads otherwise. */
|
||||
const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [
|
||||
{
|
||||
reason: 'a TTL that is no Java int is 3600',
|
||||
reason: 'a TTL that is no number, or a negative one, is 3600',
|
||||
lines: [
|
||||
String.raw`www.filius.de A x 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de A 4294967295 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de A 99999999999 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de A 2147483648 1.2.3.4\u000A`,
|
||||
],
|
||||
},
|
||||
{
|
||||
reason: 'a TTL is Java\'s Integer.parseInt: negative, and in any script\'s digits',
|
||||
lines: [
|
||||
String.raw`www.filius.de A -5 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de A -2147483648 1.2.3.4\u000A`,
|
||||
String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`,
|
||||
],
|
||||
},
|
||||
{
|
||||
reason: 'labels after the first may hold a backslash (Java\'s domain pattern)',
|
||||
lines: [
|
||||
String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`,
|
||||
],
|
||||
reason: "a TTL is read by Java's Integer.parseInt, in any script's digits",
|
||||
lines: [String.raw`www.filius.de A \u0661\u0660 1.2.3.4\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'Java trims control characters off a line (String.trim)',
|
||||
@@ -130,23 +112,52 @@ const JAVA_ANSWERS_SWIFT_DROPS: { reason: string; lines: string[] }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/** Lines where Java errs and this reader does not (see dnsHosts.ts). */
|
||||
const JAVA_BUGS: { reason: string; lines: string[] }[] = [
|
||||
{
|
||||
reason: 'a label may start with a digit (RFC 1123); Java empties the name',
|
||||
lines: [String.raw`1und1.de A 3600 1.2.3.4\u000A`, String.raw`www.1und1.de A 3600 1.2.3.4\u000A`],
|
||||
},
|
||||
{
|
||||
reason: "a label holds no backslash; Java's pattern lets one in after the first label",
|
||||
lines: [String.raw`www.a\u005Cb.de A 3600 1.2.3.4\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'the type is read in any case (RFC 4343); Java never answers "a" or "mx"',
|
||||
lines: [String.raw`www.filius.de a 3600 1.2.3.4\u000A`, String.raw`filius.de mx 3600 mail.filius.de\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'a negative TTL is invalid (RFC 2181) and so 3600; Java keeps it',
|
||||
lines: [String.raw`www.filius.de A -5 1.2.3.4\u000A`, String.raw`www.filius.de A -2147483648 1.2.3.4\u000A`],
|
||||
},
|
||||
];
|
||||
|
||||
const javaBugs = new Map(JAVA_BUGS.flatMap(({ reason, lines }) => lines.map((line) => [line, reason] as const)));
|
||||
|
||||
const departures = new Map(
|
||||
[...SWIFT_READS_JAVA_NEVER_ANSWERS, ...JAVA_ANSWERS_SWIFT_DROPS].flatMap(({ reason, lines }) =>
|
||||
lines.map((line) => [line, reason] as const),
|
||||
),
|
||||
);
|
||||
|
||||
describe('readJavaDnsHosts against Java', () => {
|
||||
describe('readDnsHosts against Java, but for its errors', () => {
|
||||
it('covers every line of the cases file, in both oracles', () => {
|
||||
const lines = readFileSync(join(here, 'dns-hosts.cases.txt'), 'ascii').split('\n').slice(0, -1);
|
||||
expect(javaMeasured.map(([line]) => line)).toEqual(lines);
|
||||
expect(swiftMeasured.map(([line]) => line)).toEqual(lines);
|
||||
});
|
||||
|
||||
it('names only cases of the cases file as Java bugs', () => {
|
||||
const lines = new Set(javaMeasured.map(([line]) => line));
|
||||
expect([...javaBugs.keys()].filter((line) => !lines.has(line))).toEqual([]);
|
||||
});
|
||||
|
||||
for (const [line, expected] of javaMeasured) {
|
||||
it(JSON.stringify(line), () => {
|
||||
const records = readJavaDnsHosts(unescape(line));
|
||||
expect(records.map((record) => [record.domainName, record.type, record.ttl, record.rdata])).toEqual(expected);
|
||||
const bug = javaBugs.get(line);
|
||||
it(bug === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (Java errs: ${bug})`, () => {
|
||||
const records = readDnsHosts(unescape(line)).map((record) => [record.domainName, record.type, record.ttl, record.rdata]);
|
||||
if (bug === undefined) expect(records).toEqual(expected);
|
||||
else expect(records).not.toEqual(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -168,7 +179,14 @@ describe('the records the model keeps, against Swift', () => {
|
||||
|
||||
describe('the records Java answers', () => {
|
||||
it('leave out records Java never answers: an empty name, or a type other than A, MX, NS', () => {
|
||||
expect(modelRows(String.raw`1und1.de A 3600 1.2.3.4\u000Afilius.de CNAME 3600 www.filius.de\u000Afilius.de a 3600 1.2.3.4`)).toEqual([]);
|
||||
expect(modelRows(String.raw`a_b.de A 3600 1.2.3.4\u000Afilius.de CNAME 3600 www.filius.de\u000A1.2.3.4 A 3600 1.2.3.4`)).toEqual([]);
|
||||
});
|
||||
|
||||
it('read a name with a digit first and a type in any case, but no numeric last label', () => {
|
||||
expect(modelRows(String.raw`1und1.de a 60 1.2.3.4\u000A1und1.de Mx 60 mx.1und1.de\u000A1und1.de MX 60 10`)).toEqual([
|
||||
['1und1.de', 'A', 60, '1.2.3.4'],
|
||||
['1und1.de', 'MX', 60, 'mx.1und1.de.'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('take names in lower case without the final dot, A targets as FILIUS normalises addresses', () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// Java's readers of the e-mail account files: every case in
|
||||
// The readers of the e-mail account files: every case in
|
||||
// email-server.cases.txt and email-client.cases.txt was read by Java
|
||||
// (EmailServer.kontenLaden, EmailAnwendung.laden; *.java.json,
|
||||
// tools/java-oracle) and must come out the same here.
|
||||
// tools/java-oracle) and must come out the same here — except where Java
|
||||
// errs, each case named with its reason.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { addressEntry, readJavaEmailClientFile, readJavaEmailServerFile } from '../src/javaEmail.js';
|
||||
import { addressEntry, readEmailClientFile, readEmailServerFile } from '../src/javaEmail.js';
|
||||
import { javaEmailAccount } from './javaEmailNames.js';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -26,22 +27,112 @@ function measured(kind: 'server' | 'client'): [string, unknown][] {
|
||||
return cases;
|
||||
}
|
||||
|
||||
describe('readJavaEmailServerFile against Java', () => {
|
||||
for (const [line, expected] of measured('server')) {
|
||||
it(JSON.stringify(line), () => {
|
||||
const accounts = readJavaEmailServerFile(unescape(line));
|
||||
expect(Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`).toEqual(expected);
|
||||
/** Server files where Java errs and this reader does not (see readEmailServerFile). */
|
||||
const SERVER_JAVA_BUGS: { reason: string; lines: string[] }[] = [
|
||||
{
|
||||
reason: "an empty field counts; Java's StringTokenizer skips it and shifts the rest",
|
||||
lines: [String.raw`bob;;bob;Nachname;Vorname;\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'a short line alone is left out; Java throws for the whole file',
|
||||
lines: [String.raw`bob;filius.de;bob;Nachname\u000A`, String.raw`bob;filius.de;bob;Nachname;Vorname;\u000Abroken\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'a message keeps an empty text or subject; Java loses it (split drops trailing empty fields)',
|
||||
lines: [
|
||||
String.raw`bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$Hi$\u000A`,
|
||||
String.raw`bob;filius.de;bob;Nachname;Vorname;#Alice <alice@senior.de>$bob@filius.de$$$$$\u000A`,
|
||||
],
|
||||
},
|
||||
{
|
||||
reason: "a \";\" in the messages belongs to them (Java writes subjects unescaped); Java's tokenizer cuts there",
|
||||
lines: [String.raw`bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text;extra\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'a CRLF line ends before its CR; Java keeps the CR in the last field',
|
||||
lines: [
|
||||
String.raw`bob;filius.de;bob;Nachname;Vorname\u000D\u000A`,
|
||||
String.raw`bob;filius.de;bob;Nachname;Vorname;#a@x$b@y$$$$Hi$Text\u000D\u000A`,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** Client files where Java errs and this reader does not (see readEmailClientFile). */
|
||||
const CLIENT_JAVA_BUGS: { reason: string; lines: string[] }[] = [
|
||||
{
|
||||
reason: 'the last complete line is the account; Java takes the last line and so loses the account',
|
||||
lines: [String.raw`a;a;110;25;first;p;L;F;first@x\u000Abroken\u000A`],
|
||||
},
|
||||
{
|
||||
reason: 'an empty address is still an account; Java loses it (split drops trailing empty fields)',
|
||||
lines: [String.raw`a;b;110;25;u;p;L;F;\u000A`, String.raw`;;;;;;;;`],
|
||||
},
|
||||
{
|
||||
reason: 'an empty file holds no account; Java makes one without any values',
|
||||
lines: [String.raw``],
|
||||
},
|
||||
{
|
||||
reason: 'a CRLF line ends before its CR; Java keeps the CR in the address',
|
||||
lines: [String.raw`a;b;110;25;u;p;L;F;u@x\u000D\u000A`],
|
||||
},
|
||||
];
|
||||
|
||||
function byLine(bugs: { reason: string; lines: string[] }[]): Map<string, string> {
|
||||
return new Map(bugs.flatMap(({ reason, lines }) => lines.map((line) => [line, reason] as const)));
|
||||
}
|
||||
|
||||
function compareWithJava(kind: 'server' | 'client', bugs: Map<string, string>, read: (content: string) => unknown) {
|
||||
const cases = measured(kind);
|
||||
it('names only cases of the cases file as Java bugs', () => {
|
||||
const lines = new Set(cases.map(([line]) => line));
|
||||
expect([...bugs.keys()].filter((line) => !lines.has(line))).toEqual([]);
|
||||
});
|
||||
for (const [line, expected] of cases) {
|
||||
const bug = bugs.get(line);
|
||||
it(bug === undefined ? JSON.stringify(line) : `${JSON.stringify(line)} (Java errs: ${bug})`, () => {
|
||||
if (bug === undefined) expect(read(unescape(line))).toEqual(expected);
|
||||
else expect(read(unescape(line))).not.toEqual(expected);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('readEmailServerFile against Java, but for its errors', () => {
|
||||
compareWithJava('server', byLine(SERVER_JAVA_BUGS), (content) => readEmailServerFile(content, () => {}).map(javaEmailAccount));
|
||||
});
|
||||
|
||||
describe('readJavaEmailClientFile against Java', () => {
|
||||
for (const [line, expected] of measured('client')) {
|
||||
it(JSON.stringify(line), () => {
|
||||
const account = readJavaEmailClientFile(unescape(line));
|
||||
expect(account === null ? null : javaEmailAccount(account)).toEqual(expected);
|
||||
});
|
||||
}
|
||||
describe('readEmailClientFile against Java, but for its errors', () => {
|
||||
compareWithJava('client', byLine(CLIENT_JAVA_BUGS), (content) => {
|
||||
const account = readEmailClientFile(content, () => {});
|
||||
return account === null ? null : javaEmailAccount(account);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the account files, where Java errs', () => {
|
||||
it('read what Java writes for an empty password, text and subject', () => {
|
||||
const warnings: string[] = [];
|
||||
const [account] = readEmailServerFile('bob;filius.de;;Nachname;Vorname;#<a@x>$<bob@filius.de>$$$$$\n', (message) => warnings.push(message));
|
||||
expect(account).toMatchObject({ username: 'bob', password: '', lastName: 'Nachname', firstName: 'Vorname' });
|
||||
expect(account!.messages).toMatchObject([{ sender: { mailAddress: 'a@x' }, subject: '', text: '' }]);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('leave out a short server line or a broken message, and say so', () => {
|
||||
const warnings: string[] = [];
|
||||
const accounts = readEmailServerFile('broken;line\nbob;filius.de;bob;N;V;#a@x$b@y$$$$Hi $5$Text\n', (message) => warnings.push(message));
|
||||
expect(accounts.map((account) => [account.username, account.messages.length])).toEqual([['bob', 0]]);
|
||||
expect(warnings).toEqual([
|
||||
'/mailserver/konten.txt: line 1 has fewer than five fields, left out',
|
||||
"/mailserver/konten.txt: a message of 'bob' has 8 fields, not seven, left out",
|
||||
]);
|
||||
});
|
||||
|
||||
it('take the last complete client line, and say which lines are not accounts', () => {
|
||||
const warnings: string[] = [];
|
||||
const account = readEmailClientFile('a;a;110;25;first;p;L;F;first@x\nbroken\n\n', (message) => warnings.push(message));
|
||||
expect(account).toMatchObject({ username: 'first', emailAddress: 'first@x' });
|
||||
expect(warnings).toEqual(['/konten.txt: line 2 is no account of nine fields, ignored']);
|
||||
expect(readEmailClientFile('a;b;110;25;u;p;L;F;\n', () => {})).toMatchObject({ username: 'u', emailAddress: '' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('addressEntry', () => {
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
type FlsNodeClass,
|
||||
type FlsTopology,
|
||||
type JavaEmailAccount,
|
||||
type JavaFailure,
|
||||
} from '../src/index.js';
|
||||
import { EXPECTED_WARNINGS } from './expectedWarnings.js';
|
||||
import { javaEmail, javaEmailAccount } from './javaEmailNames.js';
|
||||
@@ -122,9 +121,8 @@ function javaApplications(applications: FlsApplication[]): unknown[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function javaFileAccounts(accounts: JavaEmailAccount[] | JavaFailure | null): unknown {
|
||||
if (accounts === null) return null;
|
||||
return Array.isArray(accounts) ? accounts.map(javaEmailAccount) : `!${accounts.exception}`;
|
||||
function javaFileAccounts(accounts: JavaEmailAccount[] | null): unknown {
|
||||
return accounts === null ? null : accounts.map(javaEmailAccount);
|
||||
}
|
||||
|
||||
/** Java's property names, as FlsOracle.java writes them. */
|
||||
@@ -370,9 +368,9 @@ describe('readTopology rules', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('splits static DHCP assignments at Java whitespace and stops where Java throws', () => {
|
||||
// U+3000 separates, U+00A0 does not; the third entry has no IP, so Java
|
||||
// throws there and the fourth is never read.
|
||||
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">
|
||||
@@ -393,10 +391,11 @@ describe('readTopology rules', () => {
|
||||
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; Java stops reading here',
|
||||
'node 1: DHCPServer: static assignment "0A:00:00:00:00:03" has no MAC and IP, left out',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -462,7 +461,7 @@ describe('readTopology rules', () => {
|
||||
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>1und1.de</string></void>
|
||||
<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>
|
||||
@@ -473,7 +472,7 @@ describe('readTopology rules', () => {
|
||||
</void></void></object>`)}</object>`);
|
||||
const server = nodes[0]!.system.applications![0]!;
|
||||
|
||||
// setMailDomain keeps "filius.de" for a domain Java's pattern rejects.
|
||||
// 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' });
|
||||
|
||||
Reference in New Issue
Block a user