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
|
|
|
|
|
Subject: A subject
|
|
|
|
|
Date: Sat, 13 Mar 2021 11:30:40
|
|
|
|
|
|
|
|
|
|
<p>Some HTML content</p>
|
|
|
|
|
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}`,
|
|
|
|
|
subject: "A subject",
|
|
|
|
|
html: html`<p>Some HTML content</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>`
|
|
|
|
|
);
|
|
|
|
|
expect(feed).toMatch(html`<title>A subject</title>`);
|
|
|
|
|
expect(feed).toMatch(
|
|
|
|
|
// prettier-ignore
|
|
|
|
|
html`<content type="html">${`<p>Some HTML content</p>`}\n</content>`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 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", () => {
|
|
|
|
|
test("text content", async () => {
|
2020-03-20 20:08:04 +01:00
|
|
|
|
const identifier = await createFeed();
|
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: "TEXT content",
|
2020-03-19 05:20:28 +01:00
|
|
|
|
});
|
2020-03-21 16:58:28 +01:00
|
|
|
|
const feed = await getFeed(identifier);
|
2020-07-23 17:11:41 +02:00
|
|
|
|
const entry = feed.querySelector("feed > entry:first-of-type")!;
|
|
|
|
|
const alternate = await getAlternate(
|
|
|
|
|
entry.querySelector("link")!.getAttribute("href")!
|
|
|
|
|
);
|
|
|
|
|
expect(entry.querySelector("content")!.textContent).toMatch("TEXT content");
|
|
|
|
|
expect(alternate.querySelector("p")!.textContent).toMatch("TEXT content");
|
2020-03-19 05:20:28 +01:00
|
|
|
|
});
|
|
|
|
|
|
2020-03-23 22:43:48 +01:00
|
|
|
|
test("rich text content", 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-23 22:43:48 +01:00
|
|
|
|
subject: "New Message",
|
2020-08-04 23:57:49 +02:00
|
|
|
|
text: "TEXT content\n\nhttps://leafac.com\n\nMore text",
|
2020-03-23 22:43:48 +01:00
|
|
|
|
});
|
|
|
|
|
const feed = await getFeed(identifier);
|
2020-07-23 17:11:41 +02:00
|
|
|
|
const entry = feed.querySelector("feed > entry:first-of-type")!;
|
|
|
|
|
const alternate = await getAlternate(
|
|
|
|
|
entry.querySelector("link")!.getAttribute("href")!
|
|
|
|
|
);
|
|
|
|
|
expect(alternate.querySelector("a")!.getAttribute("href")).toBe(
|
2020-08-04 23:57:49 +02:00
|
|
|
|
"https://leafac.com"
|
2020-07-23 17:11:41 +02:00
|
|
|
|
);
|
2020-03-23 22:43:48 +01:00
|
|
|
|
});
|
|
|
|
|
|
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): ||💩</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
|
|
|
|
*/
|