The oracle MailWindowRun uses Java's e-mail windows as a person does and answers their dialogs. The core sets and saves the program's account, deletes a mail, checks a form as Java's window does, and gives the server its domain, new and removed accounts and the window's own line; the room sends both programs' views and takes their actions; the editor has both windows. Better than Java: a name field for a new account, a user name the server has is refused, deleting hits the folder shown, a new domain goes into the accounts file at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
14 KiB
TypeScript
254 lines
14 KiB
TypeScript
// The e-mail program's and the e-mail server's windows against Java's
|
|
// (tools/java-oracle MailWindowRun) on mail-lan's network: the same server
|
|
// switched, its domain set, accounts made and removed; the same account set
|
|
// in the program, mails written, sent, fetched, answered and deleted — the
|
|
// same rows, previews, files and lines after each. What Java's window checks
|
|
// before it acts (a form's addresses, a user name), the editor's panel checks
|
|
// with the core's functions; here they are compared with Java's dialogs.
|
|
// Named where the windows are better than Java's:
|
|
// - a user name the server has already is refused (Java's window says it
|
|
// made it),
|
|
// - deleting deletes the mail chosen in the folder shown (Java's deleted a
|
|
// mail of the other folder, where one was chosen before),
|
|
// - a new domain is written into the accounts file at once (Java writes it
|
|
// with the next change of the accounts).
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { accountOfForm, mailFormProblem, NetworkRuntimeEngine, splitName, VirtualFileSystem, withSnapshot, type AccountForm, type MailForm } from '@mdg-filius/core';
|
|
import { inboxRow, replyForm, sentRow } from '@mdg-filius/protocol';
|
|
import { snapshotOf, type Scenario } from '../../core/test/scenario.js';
|
|
|
|
interface JavaStep {
|
|
readonly step: string;
|
|
readonly [field: string]: unknown;
|
|
}
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const java = JSON.parse(readFileSync(join(here, 'mailWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
|
|
const scenario = JSON.parse(readFileSync(join(here, '../../core/test/scenarios/mail-lan.scenario.json'), 'utf8')) as Scenario;
|
|
const CLIENT = '00000000-0000-0000-0000-000000000201';
|
|
const SERVER = '00000000-0000-0000-0000-000000000203';
|
|
const step = (name: string) => java.steps.find((each) => each.step === name)!;
|
|
|
|
/** Java's window's placeholders for a new account's names (emailserver_msg21, msg20). */
|
|
const PLACEHOLDER = { firstName: 'Vorname', lastName: 'Nachname' };
|
|
|
|
function setUp() {
|
|
const engine = new NetworkRuntimeEngine(scenario.seed);
|
|
const serverFiles = new VirtualFileSystem();
|
|
const clientFiles = new VirtualFileSystem();
|
|
const snapshot = withSnapshot(snapshotOf(scenario), {
|
|
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: [], isActive: false }]]),
|
|
emailClientsByNodeId: new Map([[CLIENT, { account: null, inbox: [], sent: [], drafts: [], addressBook: [], nextMessageID: 1 }]]),
|
|
fileSystemsByNodeId: new Map([
|
|
[SERVER, serverFiles],
|
|
[CLIENT, clientFiles],
|
|
]),
|
|
});
|
|
engine.handle({ type: 'start', snapshot, seed: scenario.seed, initialTimeMilliseconds: 0 });
|
|
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
|
|
return { engine, server: engine.emailServer(SERVER)!, client: engine.emailClient(CLIENT)!, serverFiles, clientFiles, run };
|
|
}
|
|
|
|
/** The server's window as Java's shows it: button, domain field, rows of address and number of mails. */
|
|
function serverWindow(context: ReturnType<typeof setUp>) {
|
|
const { server } = context;
|
|
return {
|
|
button: server.isActive ? 'Anhalten' : 'Starten',
|
|
domain: server.domain,
|
|
domainEnabled: !server.isActive,
|
|
accounts: server.state.accounts.map((account) => [`${account.username}@${server.domain}`, String(account.mailbox.length)]),
|
|
accountsFile: context.serverFiles.textFile('/mailserver/konten.txt'),
|
|
};
|
|
}
|
|
|
|
const javaServerWindow = (each: JavaStep) => ({ button: each.button, domain: each.domain, domainEnabled: each.domainEnabled, accounts: each.accounts, accountsFile: each.accountsFile });
|
|
|
|
/** What Java's server window checks before it makes an account, as the editor's panel checks it (emailserver_msg25, msg24). */
|
|
function serverFormProblem(username: string, password: string): 'missing' | 'whiteSpace' | undefined {
|
|
if (username === '' || password === '') return 'missing';
|
|
return /[ \t\n\v\f\r]/.test(username) ? 'whiteSpace' : undefined;
|
|
}
|
|
|
|
const ACCOUNT_FORM: AccountForm = {
|
|
name: 'Anna Maria Muster',
|
|
email: 'anna@schule.de',
|
|
pop3Host: '192.168.0.30',
|
|
pop3Port: '110',
|
|
smtpHost: '192.168.0.30',
|
|
smtpPort: '25',
|
|
username: 'anna',
|
|
password: 'geheim',
|
|
};
|
|
|
|
const MAIL_FORMS: Record<string, MailForm> = {
|
|
invalid: { to: 'bob@schule.de, kein', cc: '', bcc: '', subject: 'Falsch', text: 'x' },
|
|
empty: { to: '', cc: '', bcc: '', subject: 'Leer', text: 'x' },
|
|
hello: { to: 'bob@schule.de', cc: '', bcc: '', subject: 'Hallo', text: 'Zeile 1\nZeile 2' },
|
|
self: { to: 'anna@schule.de', cc: '', bcc: '', subject: 'Selbst', text: 'Erste Zeile\nZweite Zeile' },
|
|
};
|
|
|
|
describe("the e-mail windows, against Java's", () => {
|
|
it("switch the server, set its domain and make its accounts as Java's window does", () => {
|
|
const context = setUp();
|
|
const { server } = context;
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:open')));
|
|
expect(server.setDomain('ungültig domain!')).toBe(false);
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:domain ungültig domain!')));
|
|
expect(server.setDomain('schule.de')).toBe(true);
|
|
expect(serverWindow(context)).toEqual({ ...javaServerWindow(step('server:domain schule.de')), accountsFile: '' });
|
|
server.setActive(true, true);
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:start')));
|
|
expect(server.log.all.map((line) => line.kind)).toEqual(['switched']);
|
|
const dialogs: Record<string, string> = {
|
|
missing: 'Geben Sie einen Benutzernamen und ein Passwort ein!',
|
|
whiteSpace: 'Der Benutzername darf keine Leerzeichen enthalten!',
|
|
};
|
|
for (const [username, password] of [['anna', 'geheim'], ['', 'x'], ['carl os', 'x'], ['ANNA', 'anders'], ['bob', ''], ['bob', 'pw']] as const) {
|
|
const recorded = step(`server:add ${username}/${password}`);
|
|
const problem = serverFormProblem(username, password);
|
|
if (problem !== undefined) {
|
|
expect(recorded.dialogs, username).toEqual([dialogs[problem]]);
|
|
} else {
|
|
const made = server.addAccount({ username, password, ...PLACEHOLDER });
|
|
// Java's window says it made "ANNA"; the server has "anna" already and takes nothing.
|
|
expect(recorded.dialogs).toEqual([`Das Benutzerkonto ${username} wurde angelegt.`]);
|
|
expect(made, username).toBe(username !== 'ANNA');
|
|
}
|
|
expect(serverWindow(context), username).toEqual(javaServerWindow(recorded));
|
|
}
|
|
});
|
|
|
|
it("set the program's account, write and send its mails, fetch, answer and delete them as Java's window does", () => {
|
|
const context = setUp();
|
|
const { server, client, clientFiles, run } = context;
|
|
server.setDomain('schule.de');
|
|
server.setActive(true, true);
|
|
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
|
|
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
|
|
expect(client.account).toBeNull();
|
|
|
|
for (const [username, port] of [['an na', '110'], ['anna', '110x'], ['anna', '110']] as const) {
|
|
const recorded = step(`client:account ${username} ${port}`);
|
|
const account = accountOfForm({ ...ACCOUNT_FORM, username, pop3Port: port });
|
|
if (account === undefined) expect(recorded.dialogs).toEqual(['Die Angaben sind entweder fehlerhaft oder unvollständig!']);
|
|
else client.setAccount(account);
|
|
expect(clientFiles.contains('/konten.txt') ? clientFiles.textFile('/konten.txt') : null, username + port).toBe(recorded.accountFile);
|
|
}
|
|
// Opened again, the dialog shows the account: first and last name with a space between.
|
|
const shown = client.account!;
|
|
expect([`${shown.firstName} ${shown.lastName}`, shown.email, shown.pop3Host, String(shown.pop3Port), shown.smtpHost, String(shown.smtpPort), shown.username, shown.password]).toEqual(
|
|
step('client:account shown').fields,
|
|
);
|
|
|
|
const problems: Record<string, string> = {
|
|
invalidAddress: 'Mindestens eine eingegebene E-Mail-Adresse ist nicht korrekt!',
|
|
noRecipient: 'Sie müssen mindestens eine Empfänger-Adresse eingeben!',
|
|
};
|
|
for (const form of Object.values(MAIL_FORMS)) {
|
|
const name = form.subject;
|
|
const recorded = step(`client:send ${name}`);
|
|
const problem = mailFormProblem(form, true);
|
|
if (problem !== undefined) {
|
|
expect(recorded.dialogs, name).toEqual([problems[problem]]);
|
|
// The compose window stays open with what was typed.
|
|
expect([recorded.composing, recorded.composeFields, recorded.composeText, recorded.composeSender]).toEqual([true, [form.to, form.cc, form.bcc, form.subject], form.text, 'anna']);
|
|
} else {
|
|
expect(client.send(form)).toBeUndefined();
|
|
run(20_000);
|
|
// Sent: the compose window closes, the window shows the sent mails.
|
|
expect([recorded.composing, recorded.tab]).toEqual([false, 1]);
|
|
}
|
|
expect(client.sent.map((entry) => sentRow(entry.mail)), name).toEqual(recorded.sent);
|
|
}
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:after mails')));
|
|
|
|
client.fetch();
|
|
run(20_000);
|
|
const fetched = step('client:fetch');
|
|
expect([client.inbox.map((entry) => inboxRow(entry.mail)), fetched.tab]).toEqual([fetched.inbox, 0]);
|
|
expect(client.inbox[0]!.mail.text).toBe(step('client:choose inbox 0').preview);
|
|
const answer = replyForm(client.inbox[0]!.mail, { re: 'RE:', wrote: 'schrieb:' });
|
|
const answered = step('client:answer');
|
|
expect([[answer.to, '', '', answer.subject], answer.text, 'anna']).toEqual([answered.composeFields, answered.composeText, answered.composeSender]);
|
|
expect(client.sent[0]!.mail.text).toBe(step('client:choose sent 0').preview);
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:after fetch')));
|
|
|
|
// The mail chosen in the inbox is deleted (Java deleted the sent mail chosen before).
|
|
client.removeMail('inbox', 0);
|
|
const deleted = step('client:delete inbox 0');
|
|
expect(client.inbox).toEqual([]);
|
|
expect(deleted.inbox).toEqual(fetched.inbox);
|
|
expect(client.sent.map((entry) => sentRow(entry.mail))).toEqual((fetched.sent as unknown[]).slice());
|
|
expect(deleted.sent).toEqual((fetched.sent as unknown[]).slice(1));
|
|
// A row it has not: nothing.
|
|
client.removeMail('sent', 5);
|
|
client.removeMail('sent', -1);
|
|
expect(client.sent).toHaveLength(2);
|
|
});
|
|
|
|
it("remove an account, stop the server and set its domain while it is off, as Java's window does", () => {
|
|
const context = setUp();
|
|
const { server } = context;
|
|
server.setDomain('schule.de');
|
|
server.setActive(true, true);
|
|
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
|
|
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
|
|
expect(step('server:delete row 1').dialogs).toEqual(["Wollen Sie das Konto 'bob' wirklich entfernen?"]);
|
|
expect(server.removeAccount('bob')).toBe(true);
|
|
expect(server.removeAccount('bob')).toBe(false);
|
|
expect(server.removeAccount('ANNA')).toBe(false);
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:delete row 1')));
|
|
server.setActive(false, true);
|
|
expect(serverWindow(context)).toEqual(javaServerWindow(step('server:stop')));
|
|
expect(server.log.all.map((line) => line.kind)).toEqual(['switched', 'switched']);
|
|
expect(server.log.all.map((line) => line.kind === 'switched' && line.active)).toEqual([true, false]);
|
|
expect(server.setDomain('neu.de')).toBe(true);
|
|
// The new domain goes into the file at once (Java's keeps the old one until the accounts change).
|
|
const recorded = step('server:domain neu.de');
|
|
expect(serverWindow(context)).toEqual({ ...javaServerWindow(recorded), accountsFile: 'anna;neu.de;geheim;Nachname;Vorname;\n' });
|
|
expect(recorded.accountsFile).toBe('anna;schule.de;geheim;Nachname;Vorname;\n');
|
|
expect(server.setDomain('')).toBe(false);
|
|
expect(server.domain).toBe('neu.de');
|
|
});
|
|
|
|
it("split a name as Java's account dialog does", () => {
|
|
expect(splitName('Anna Maria Muster')).toEqual({ firstName: 'Anna Maria', lastName: 'Muster' });
|
|
expect(splitName('Anna')).toEqual({ firstName: 'Anna', lastName: '' });
|
|
expect(splitName(' Anna ')).toEqual({ firstName: '', lastName: 'Anna' });
|
|
expect(splitName('Anna ')).toEqual({ firstName: 'Anna', lastName: '' });
|
|
expect(splitName(' ')).toEqual({ firstName: '', lastName: '' });
|
|
expect(accountOfForm({ ...ACCOUNT_FORM, username: '', smtpPort: '65535', pop3Port: '1' })).toMatchObject({ username: '', smtpPort: 65535, pop3Port: 1 });
|
|
for (const port of ['0', '65536', '', '080', ' 25']) expect(accountOfForm({ ...ACCOUNT_FORM, smtpPort: port }), port).toBeUndefined();
|
|
expect(accountOfForm({ ...ACCOUNT_FORM, username: 'a.b_c-d9' })).toBeDefined();
|
|
expect(accountOfForm({ ...ACCOUNT_FORM, username: 'anna@x' })).toBeUndefined();
|
|
});
|
|
|
|
it('deletes a sent mail, sends with the account a mail was written with, and keeps a new domain in the snapshot', () => {
|
|
const context = setUp();
|
|
const { engine, server, client, run } = context;
|
|
server.setDomain('schule.de');
|
|
expect(engine.state.topologySnapshot.emailServersByNodeId.get(SERVER)!.domain).toBe('schule.de');
|
|
server.setActive(true, true);
|
|
server.addAccount({ username: 'anna', password: 'geheim', ...PLACEHOLDER });
|
|
server.addAccount({ username: 'bob', password: 'pw', ...PLACEHOLDER });
|
|
client.setAccount(accountOfForm(ACCOUNT_FORM)!);
|
|
expect(client.send({ ...MAIL_FORMS.hello!, to: 'bob@schule.de, anna@schule.de' })).toBeUndefined();
|
|
// Changed before the mail goes to its second recipient: it still goes with the account it was written with.
|
|
client.setAccount(accountOfForm({ ...ACCOUNT_FORM, smtpHost: 'niemand.de' })!);
|
|
run(40_000);
|
|
expect(client.sent.map((entry) => entry.mail.subject)).toEqual(['Hallo']);
|
|
expect(server.state.accounts.map((account) => account.mailbox.length)).toEqual([1, 1]);
|
|
client.setAccount(accountOfForm(ACCOUNT_FORM)!);
|
|
expect(client.send(MAIL_FORMS.self!)).toBeUndefined();
|
|
run(20_000);
|
|
client.removeMail('sent', 0);
|
|
expect(client.sent.map((entry) => entry.mail.subject)).toEqual(['Selbst']);
|
|
expect(engine.state.topologySnapshot.emailClientsByNodeId.get(CLIENT)!.sent.map((message) => message.subject)).toEqual(['Selbst']);
|
|
});
|
|
});
|
|
|