feat(core): the e-mail server after Java, measured against Java
SMTP on port 25 stores mails for the domain's accounts as Java reads them anew and tells a local sender of unknown recipients; POP3 on port 110 hands them out. The mailboxes are in the snapshot and go back into the project. EmailRun plays four stories over the network; MailCases feeds Java's workers without one (26 cases, de and en). 18 cases name Java bugs that the core does not take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// The e-mail server as Java FILIUS runs it. tools/java-oracle's EmailRun let
|
||||
// FILIUS' generic client talk SMTP and POP3 to an e-mail server on
|
||||
// mail-lan's network and wrote every frame each card saw, Java's tables of
|
||||
// them, the lines of the server's and the client's windows, and the
|
||||
// server's accounts at the end (email-java-<story>.java.json). The engine
|
||||
// plays the same stories with its programs (apps/emailServer.ts, the
|
||||
// generic client of apps/echo.ts): each card's frames in the same order,
|
||||
// with the same content and rows, at about the same time; each window's
|
||||
// lines as Java's, at about the same time; the same accounts at the end.
|
||||
// Java's errors in these dialogues are measured without a network
|
||||
// (emailServer.test.ts); these stories keep to what Java does right.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatMail, withSnapshot, type LogLine, type NetworkRuntimeEngine } from '../src/index.js';
|
||||
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
|
||||
import { MAIL_ACCOUNTS } from './mailAccounts.js';
|
||||
import { clientText, emailServerText, isPolled, POLL_MILLISECONDS } from './programLines.js';
|
||||
import { snapshotOf, type Scenario } from './scenario.js';
|
||||
|
||||
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
|
||||
|
||||
const CLIENT = '00000000-0000-0000-0000-000000000201';
|
||||
const SERVER = '00000000-0000-0000-0000-000000000203';
|
||||
|
||||
type Program = 'server' | 'client';
|
||||
|
||||
interface JavaRun extends JavaTables {
|
||||
frames: Record<string, JavaFrame[]>;
|
||||
events: [number, Program, string][];
|
||||
/** Each account's name and its messages: Email.toString, new, marked for deletion. */
|
||||
accounts: [string, ...[string, string, string][]][];
|
||||
}
|
||||
|
||||
type Act = (engine: NetworkRuntimeEngine) => void;
|
||||
|
||||
/** EmailRun's calls at their times: connecting takes 2.5 s, each message 1.5 s, from 500 ms on. */
|
||||
function story(...parts: (readonly ['connect', number] | readonly ['send', ...string[]] | readonly ['wait', number])[]): [number, Act][] {
|
||||
const acts: [number, Act][] = [];
|
||||
let at = 500;
|
||||
for (const [kind, ...rest] of parts) {
|
||||
if (kind === 'connect') {
|
||||
const port = rest[0] as number;
|
||||
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.connect('192.168.0.30', port)]);
|
||||
at += 2_500;
|
||||
} else if (kind === 'send') {
|
||||
for (const text of rest as string[]) {
|
||||
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.send(text)]);
|
||||
at += 1_500;
|
||||
}
|
||||
} else {
|
||||
at += rest[0] as number;
|
||||
}
|
||||
}
|
||||
return acts;
|
||||
}
|
||||
|
||||
const STORIES: Record<string, [number, Act][]> = {
|
||||
smtp: story(
|
||||
['connect', 25],
|
||||
[
|
||||
'send',
|
||||
'HELO 192.168.0.20',
|
||||
'MAIL FROM: <bob@filius.de>',
|
||||
'RCPT TO:<anna@filius.de>',
|
||||
'DATA',
|
||||
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Gruß\n\nHallo Anna,\nwie geht's?\r\n.\r\n",
|
||||
'QUIT',
|
||||
],
|
||||
),
|
||||
smtpErrors: story(
|
||||
['connect', 25],
|
||||
[
|
||||
'send',
|
||||
'NOOP',
|
||||
'EHLO',
|
||||
'DATA',
|
||||
'MAIL FROM: bob@filius.de',
|
||||
'RCPT TO:<niemand@filius.de>',
|
||||
'DATA',
|
||||
'MAIL FROM:<bob@filius.de>',
|
||||
'RCPT TO: anna',
|
||||
'NOOP',
|
||||
'DATA',
|
||||
'From: <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?\n.\n',
|
||||
'QUIT',
|
||||
],
|
||||
),
|
||||
pop3: story(
|
||||
['connect', 110],
|
||||
['send', 'USER anna', 'PASS geheim', 'STAT', 'LIST', 'RETR 0', 'RETR 1', 'DELE 0', 'DELE 1', 'QUIT'],
|
||||
['wait', 1_000],
|
||||
['connect', 110],
|
||||
['send', 'USER anna', 'PASS geheim', 'STAT', 'QUIT'],
|
||||
),
|
||||
pop3Errors: story(['connect', 110], ['send', 'NOOP', 'USER niemand', 'PASS x', 'USER anna', 'PASS falsch', 'PASS geheim', 'NOOP', 'USER bob', 'QUIT']),
|
||||
};
|
||||
|
||||
/** A notice's date: Java's in its locale and the time it ran. */
|
||||
const withoutDate = (text: string) => text.replace(/\nDate: [^\n]*\n/g, '\nDate: <date>\n');
|
||||
|
||||
type Lines = Record<Program, [number, string][]>;
|
||||
|
||||
function javaLines(java: JavaRun): Lines {
|
||||
const lines: Lines = { server: [], client: [] };
|
||||
for (const [time, program, text] of java.events) {
|
||||
// Anwendung.starten and the server's refreshes tell their observers nothing (null), which the windows skip.
|
||||
if (text === 'null' || text.startsWith('call ')) continue;
|
||||
lines[program].push([time, withClientPortText(text)]);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function engineLines(engine: NetworkRuntimeEngine): Lines {
|
||||
const lines = <E>(log: readonly LogLine<E>[], text: (entry: E) => string): [number, string][] =>
|
||||
log.map((line) => [line.timeMilliseconds, withClientPortText(text(line))]);
|
||||
return { server: lines(engine.emailServer(SERVER)!.log.all, (entry) => emailServerText(entry)), client: lines(engine.simpleClient(CLIENT)!.log.all, clientText) };
|
||||
}
|
||||
|
||||
describe('the e-mail server as Java FILIUS runs it', () => {
|
||||
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'mail-lan.scenario.json'), 'utf8')) as Scenario;
|
||||
const snapshot = withSnapshot(snapshotOf(scenario), {
|
||||
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: MAIL_ACCOUNTS, isActive: true }]]),
|
||||
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 25 }]]),
|
||||
});
|
||||
|
||||
for (const [name, acts] of Object.entries(STORIES)) {
|
||||
it(name, () => {
|
||||
const java = JSON.parse(readFileSync(join(scenarioDirectory, `email-java-${name}.java.json`), 'utf8')) as JavaRun;
|
||||
const { engine } = watchedEngine(scenario, snapshot);
|
||||
for (const [time, act] of acts) {
|
||||
engine.handle({ type: 'advance', toMilliseconds: time });
|
||||
act(engine);
|
||||
}
|
||||
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
|
||||
|
||||
expectSameCaptures(engine, scenario, java.frames, { text: (text) => withoutDate(withClientPortText(text)), tables: java, cell: withoutDate });
|
||||
|
||||
const expected = javaLines(java);
|
||||
const actual = engineLines(engine);
|
||||
for (const program of ['server', 'client'] as const) {
|
||||
expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text));
|
||||
for (const [index, [javaTime, text]] of expected[program].entries()) {
|
||||
const late = javaTime - actual[program][index]![0];
|
||||
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + GAP_TOLERANCE_MILLISECONDS;
|
||||
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeGreaterThanOrEqual(-GAP_TOLERANCE_MILLISECONDS);
|
||||
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeLessThanOrEqual(most);
|
||||
}
|
||||
}
|
||||
|
||||
const accounts = engine.emailServer(SERVER)!.state.accounts.map((account) => [
|
||||
account.username,
|
||||
...account.mailbox.map((entry) => [withoutDate(formatMail(entry.mail)), String(entry.isNew), String(entry.isMarkedForDeletion)]),
|
||||
]);
|
||||
expect(accounts).toEqual(java.accounts.map(([username, ...mails]) => [username, ...mails.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted])]));
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user