kill-the-newsletter/src/index.test.ts

248 lines
8.6 KiB
TypeScript
Raw Normal View History

2021-03-13 12:45:51 +01:00
/*
You may send emails manually from the command line with the following:
cat << "EOF" > /tmp/example-email.txt
From: Publisher <publisher@example.com>
To: ru9rmeebswmcy7wx@localhost
2021-03-14 00:01:59 +01:00
Subject: Test email with HTML
2021-03-13 12:45:51 +01:00
Date: Sat, 13 Mar 2021 11:30:40
2021-03-14 00:01:59 +01:00
<p>Some HTML</p>
2021-03-13 12:45:51 +01:00
EOF
curl smtp://localhost:2525 --mail-from publisher@example.com --mail-rcpt ru9rmeebswmcy7wx@localhost --upload-file /tmp/example-email.txt
*/
2021-03-13 23:35:02 +01:00
import { test, expect } from "@jest/globals";
2021-03-13 11:45:13 +01:00
import os from "os";
import path from "path";
import fs from "fs";
import * as got from "got";
2020-03-19 15:48:31 +01:00
import nodemailer from "nodemailer";
2021-03-13 12:07:56 +01:00
import html from "@leafac/html";
2021-03-13 11:45:13 +01:00
import killTheNewsletter from ".";
2021-03-13 23:35:02 +01:00
test("Kill the Newsletter!", async () => {
// Start servers
2021-03-13 11:45:13 +01:00
const rootDirectory = fs.mkdtempSync(
path.join(os.tmpdir(), "kill-the-newsletter--test--")
);
const { webApplication, emailApplication } = killTheNewsletter(rootDirectory);
2021-03-13 23:35:02 +01:00
const webServer = webApplication.listen(
new URL(webApplication.get("url")).port
);
const emailServer = emailApplication.listen(
2021-03-13 11:45:13 +01:00
new URL(webApplication.get("email")).port
);
2021-03-13 23:35:02 +01:00
const webClient = got.default.extend({
prefixUrl: webApplication.get("url"),
});
const emailClient = nodemailer.createTransport(webApplication.get("email"));
const emailHost = new URL(webApplication.get("url")).hostname;
2020-03-18 20:21:44 +01:00
2021-03-13 23:35:02 +01:00
// Create feed
const create = (await webClient.post("", { form: { name: "A newsletter" } }))
.body;
expect(create).toMatch(`“A newsletter” inbox created`);
const feedReference = create.match(/\/feeds\/([a-z0-9]{16})\.xml/)![1];
// Test feed properties
let feedOriginal = await webClient.get(`feeds/${feedReference}.xml`);
expect(feedOriginal.headers["content-type"]).toMatch("application/atom+xml");
expect(feedOriginal.headers["x-robots-tag"]).toBe("noindex");
expect(feedOriginal.body).toMatch(html`<title>A newsletter</title>`);
// Test alternate
const alternateReference = feedOriginal.body.match(
2021-03-13 12:07:56 +01:00
/\/alternates\/([a-z0-9]{16})\.html/
)![1];
2021-03-13 23:35:02 +01:00
const alternate = await webClient.get(
2021-03-13 12:07:56 +01:00
`alternates/${alternateReference}.html`
2020-07-23 17:11:41 +02:00
);
2021-03-13 23:35:02 +01:00
expect(alternate.headers["content-type"]).toMatch("text/html");
expect(alternate.headers["x-robots-tag"]).toBe("noindex");
expect(alternate.body).toMatch(`Enjoy your readings!`);
2020-03-19 01:16:00 +01:00
2021-03-13 23:35:02 +01:00
// Test email with HTML
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for a second to test that the <updated> field will be updated
await emailClient.sendMail({
from: "publisher@example.com",
to: `${feedReference}@${emailHost}`,
2021-03-14 00:01:59 +01:00
subject: "Test email with HTML",
html: html`<p>Some HTML</p>`,
2021-03-13 12:23:02 +01:00
});
2021-03-13 23:35:02 +01:00
let feed = (await webClient.get(`feeds/${feedReference}.xml`)).body;
expect(feed.match(/<updated>(.+?)<\/updated>/)![1]).not.toBe(
feedOriginal.body.match(/<updated>(.+?)<\/updated>/)![1]
);
expect(feed).toMatch(
html`<author><name>publisher@example.com</name></author>`
);
2021-03-14 00:01:59 +01:00
expect(feed).toMatch(html`<title>Test email with HTML</title>`);
2021-03-13 23:35:02 +01:00
expect(feed).toMatch(
// prettier-ignore
2021-03-14 00:01:59 +01:00
html`<content type="html">${`<p>Some HTML</p>`}\n</content>`
);
// Test email with plain text
await emailClient.sendMail({
from: "publisher@example.com",
to: `${feedReference}@${emailHost}`,
subject: "Test email with plain text",
text: "Some plain text",
});
feed = (await webClient.get(`feeds/${feedReference}.xml`)).body;
expect(feed).toMatch(
html`<content type="html">${`<p>Some plain text</p>`}</content>`
);
// Test email with rich text
await emailClient.sendMail({
from: "publisher@example.com",
to: `${feedReference}@${emailHost}`,
subject: "Test email with rich text",
text: "A link: https://kill-the-newsletter.com",
});
feed = (await webClient.get(`feeds/${feedReference}.xml`)).body;
expect(feed).toMatch(
// prettier-ignore
html`<content type="html">${`<p>A link: <a href="https://kill-the-newsletter.com">https://kill-the-newsletter.com</a></p>`}</content>`
2021-03-13 23:35:02 +01:00
);
// Stop servers
webServer.close();
emailServer.close();
2021-03-13 12:23:02 +01:00
});
2021-03-13 12:07:56 +01:00
/*
2020-03-19 05:20:28 +01:00
describe("receive email", () => {
2020-03-31 20:14:44 +02:00
test("invalid XML character in HTML", async () => {
const identifier = await createFeed();
await emailClient.sendMail({
from: "publisher@example.com",
2020-04-06 15:48:34 +02:00
to: `${identifier}@${EMAIL_DOMAIN}`,
2020-03-31 20:14:44 +02:00
subject: "New Message",
2020-05-05 08:12:57 +02:00
html: "<p>Invalid XML character (backspace): |\b|💩</p>",
2020-03-31 20:14:44 +02:00
});
const feed = await getFeed(identifier);
2020-07-23 17:11:41 +02:00
const entry = feed.querySelector("feed > entry:first-of-type")!;
expect(entry.querySelector("content")!.textContent).toMatchInlineSnapshot(`
"<p>Invalid XML character (backspace): ||💩</p>
"
`);
2020-03-31 20:14:44 +02:00
});
test("invalid XML character in text", async () => {
const identifier = await createFeed();
await emailClient.sendMail({
from: "publisher@example.com",
2020-04-06 15:48:34 +02:00
to: `${identifier}@${EMAIL_DOMAIN}`,
2020-03-31 20:14:44 +02:00
subject: "New Message",
2020-05-05 08:12:57 +02:00
text: "Invalid XML character (backspace): |\b|💩",
2020-03-31 20:14:44 +02:00
});
const feed = await getFeed(identifier);
2020-07-23 17:11:41 +02:00
const entry = feed.querySelector("feed > entry:first-of-type")!;
expect(entry.querySelector("content")!.textContent).toMatchInlineSnapshot(
`"<p>Invalid XML character (backspace): |&#x8;|&#x1F4A9;</p>"`
2020-05-05 08:12:57 +02:00
);
2020-03-31 20:14:44 +02:00
});
2020-07-23 17:11:41 +02:00
test("missing from", async () => {
2020-03-23 16:08:57 +01:00
const identifier = await createFeed();
await emailClient.sendMail({
2020-04-06 15:48:34 +02:00
to: `${identifier}@${EMAIL_DOMAIN}`,
2020-04-03 23:06:43 +02:00
subject: "New Message",
2020-07-23 17:11:41 +02:00
html: "<p>HTML content</p>",
2020-03-23 16:08:57 +01:00
});
const feed = await getFeed(identifier);
2020-07-23 17:11:41 +02:00
const entry = feed.querySelector("feed > entry:first-of-type")!;
expect(entry.querySelector("author > name")!.textContent).toBe("");
expect(entry.querySelector("title")!.textContent).toBe("New Message");
});
test("nonexistent to", async () => {
await emailClient.sendMail({
from: "publisher@example.com",
to: `nonexistent@${EMAIL_DOMAIN}`,
subject: "New Message",
html: "<p>HTML content</p>",
});
2020-03-23 16:08:57 +01:00
});
2020-07-23 17:11:41 +02:00
test("missing subject", async () => {
2020-03-23 16:08:57 +01:00
const identifier = await createFeed();
await emailClient.sendMail({
from: "publisher@example.com",
2020-04-06 15:48:34 +02:00
to: `${identifier}@${EMAIL_DOMAIN}`,
2020-04-03 23:06:43 +02:00
html: "<p>HTML content</p>",
2020-03-23 16:08:57 +01:00
});
const feed = await getFeed(identifier);
2020-07-23 17:11:41 +02:00
const entry = feed.querySelector("feed > entry:first-of-type")!;
expect(entry.querySelector("title")!.textContent).toBe("");
expect(entry.querySelector("author > name")!.textContent).toBe(
"publisher@example.com"
);
});
test("missing content", async () => {
const identifier = await createFeed();
await emailClient.sendMail({
from: "publisher@example.com",
to: `${identifier}@${EMAIL_DOMAIN}`,
subject: "New Message",
});
const feed = await getFeed(identifier);
const entry = feed.querySelector("feed > entry:first-of-type")!;
expect(entry.querySelector("content")!.textContent!.trim()).toBe("");
expect(entry.querySelector("title")!.textContent).toBe("New Message");
2020-03-23 16:08:57 +01:00
});
2020-03-19 05:20:28 +01:00
test("truncation", async () => {
2020-03-20 20:08:04 +01:00
const identifier = await createFeed();
2020-07-23 17:11:41 +02:00
const alternatesURLs = new Array<string>();
for (const repetition of [...new Array(4).keys()]) {
2020-03-21 16:58:28 +01:00
await emailClient.sendMail({
2020-03-19 05:20:28 +01:00
from: "publisher@example.com",
2020-04-06 15:48:34 +02:00
to: `${identifier}@${EMAIL_DOMAIN}`,
2020-03-19 05:20:28 +01:00
subject: "New Message",
2020-04-03 23:06:43 +02:00
text: `REPETITION ${repetition} `.repeat(10_000),
2020-03-19 05:20:28 +01:00
});
2020-07-23 17:11:41 +02:00
const feed = await getFeed(identifier);
const entry = feed.querySelector("feed > entry:first-of-type")!;
alternatesURLs.push(entry.querySelector("link")!.getAttribute("href")!);
}
2020-03-21 16:58:28 +01:00
const feed = await getFeed(identifier);
2020-07-23 17:11:41 +02:00
expect(
feed.querySelector("entry:first-of-type > content")!.textContent
).toMatch("REPETITION 3");
expect(
feed.querySelector("entry:last-of-type > content")!.textContent
).toMatch("REPETITION 1");
expect((await getAlternate(alternatesURLs[3]!)).textContent).toMatch(
"REPETITION 3"
);
await expect(getAlternate(alternatesURLs[0]!)).rejects.toThrowError();
2020-03-21 16:58:28 +01:00
});
2020-03-22 15:23:05 +01:00
2020-05-05 08:12:57 +02:00
test("too big entry", async () => {
const identifier = await createFeed();
await emailClient.sendMail({
from: "publisher@example.com",
to: `${identifier}@${EMAIL_DOMAIN}`,
subject: "New Message",
2020-07-23 17:11:41 +02:00
text: "TOO BIG".repeat(100_000),
2020-05-05 08:12:57 +02:00
});
2020-07-23 17:11:41 +02:00
expect((await getFeed(identifier)).querySelector("entry")).toBeNull();
2020-05-05 08:12:57 +02:00
await emailClient.sendMail({
from: "publisher@example.com",
to: `${identifier}@${EMAIL_DOMAIN}`,
subject: "New Message",
text: `NORMAL SIZE`,
});
2020-07-23 17:11:41 +02:00
expect(
(await getFeed(identifier)).querySelector("entry > content")!.textContent
).toMatchInlineSnapshot(`"<p>NORMAL SIZE</p>"`);
2020-03-23 16:24:14 +01:00
});
2020-03-19 05:20:28 +01:00
});
2021-03-13 12:07:56 +01:00
*/