Files
jobtrackingapp/job-tracker-ui/scripts/localSMTPfixture.test.mjs
T

97 lines
5.0 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { createConnection } from "node:net";
import { decodePlainMessage, startLocalSMTPFixture } from "./localSMTPfixture.mjs";
async function client(port) {
const socket = createConnection({ host: "127.0.0.1", port });
socket.setEncoding("utf8");
let buffer = "";
const lines = [];
const waiters = [];
socket.on("data", (chunk) => {
buffer += chunk;
while (buffer.includes("\n")) {
const end = buffer.indexOf("\n");
const line = buffer.slice(0, end).replace(/\r$/, "");
buffer = buffer.slice(end + 1);
if (waiters.length) waiters.shift()(line);
else lines.push(line);
}
});
const readLine = () => lines.length ? Promise.resolve(lines.shift()) : new Promise((resolve) => waiters.push(resolve));
const reply = async () => {
let line;
do { line = await readLine(); } while (/^\d{3}-/.test(line));
return Number.parseInt(line, 10);
};
assert.equal(await reply(), 220);
return { socket, reply, command: async (line) => { socket.write(line + "\r\n"); return reply(); } };
}
test("loopback SMTP captures fragmented commands and dot-stuffed MIME for the envelope recipient only", { timeout: 5000 }, async (t) => {
const fixture = await startLocalSMTPFixture();
t.after(() => fixture.close());
assert.ok(fixture.smtpPort > 0);
const smtp = await client(fixture.smtpPort);
t.after(() => smtp.socket.destroy());
smtp.socket.write("EH");
await new Promise((resolve) => setImmediate(resolve));
smtp.socket.write("LO fixture\r\n");
assert.equal(await smtp.reply(), 250);
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
assert.equal(await smtp.command("RCPT TO:<outside@example.com>"), 550);
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
assert.equal(await smtp.command("DATA"), 354);
smtp.socket.write("Content-Type: text/plain; charset=utf-8\r\n\r\n..leading dot\r\nmessage\r\n.\r\n");
assert.equal(await smtp.reply(), 250);
assert.equal(await smtp.command("QUIT"), 221);
const response = await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`);
assert.equal(response.headers.get("cache-control"), "no-store");
assert.equal(response.headers.get("access-control-allow-origin"), null);
assert.deepEqual(await response.json(), [{ sequence: 1, body: ".leading dot\r\nmessage\r\n" }]);
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=other%40example.test`)).json(), []);
});
test("SMTP reset discards the previous envelope and rejects DATA without a recipient", { timeout: 5000 }, async (t) => {
const fixture = await startLocalSMTPFixture();
t.after(() => fixture.close());
assert.ok(fixture.smtpPort > 0);
const smtp = await client(fixture.smtpPort);
t.after(() => smtp.socket.destroy());
assert.equal(await smtp.command("DATA"), 503);
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
assert.equal(await smtp.command("RSET"), 250);
assert.equal(await smtp.command("DATA"), 503);
assert.equal(await smtp.command("QUIT"), 221);
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`)).json(), []);
});
test("SMTP oversized messages are rejected without retaining a partial recovery body", { timeout: 5000 }, async (t) => {
const fixture = await startLocalSMTPFixture();
t.after(() => fixture.close());
assert.ok(fixture.smtpPort > 0);
const smtp = await client(fixture.smtpPort);
t.after(() => smtp.socket.destroy());
assert.equal(await smtp.command("MAIL FROM:<sender@example.test>"), 250);
assert.equal(await smtp.command("RCPT TO:<reader@example.test>"), 250);
assert.equal(await smtp.command("DATA"), 354);
smtp.socket.write("Content-Type: text/plain\r\n\r\n" + "x".repeat(132000) + "\r\n.\r\n");
assert.equal(await smtp.reply(), 552);
assert.deepEqual(await (await fetch(`http://127.0.0.1:${fixture.controlPort}/messages?recipient=reader%40example.test`)).json(), []);
});
test("quoted-printable SMTP mail retains folded reset URLs and UTF-8 copy", () => {
const raw = "Subject: Verify email\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\nBekreft =C3=B8\r\nhttp://localhost:3300/verify-email?userId=3Done&to=\r\nken=3DA%2BB\r\n";
assert.equal(decodePlainMessage(raw), "Bekreft ø\r\nhttp://localhost:3300/verify-email?userId=one&token=A%2BB\r\n");
});
test("base64 SMTP mail is decoded after header unfolding", () => {
assert.equal(decodePlainMessage("Content-Type: text/plain;\r\n charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\nSGVsbG8sIHdvcmxkIQ==\r\n"), "Hello, world!");
});
test("unsupported MIME does not silently produce a recovery URL", () => {
assert.throws(() => decodePlainMessage("Content-Type: multipart/mixed; boundary=x\r\n\r\n--x\r\n"), /text\/plain/);
});