2021-03-13 11:45:13 +01:00
|
|
|
|
import { beforeAll, afterAll, describe, test, expect } from "@jest/globals";
|
|
|
|
|
import os from "os";
|
|
|
|
|
import path from "path";
|
|
|
|
|
import http from "http";
|
|
|
|
|
import net from "net";
|
|
|
|
|
import fs from "fs";
|
|
|
|
|
import * as got from "got";
|
2020-03-19 15:48:31 +01:00
|
|
|
|
import nodemailer from "nodemailer";
|
2021-03-13 11:45:13 +01:00
|
|
|
|
import killTheNewsletter from ".";
|
|
|
|
|
|
|
|
|
|
let webServer: http.Server;
|
|
|
|
|
let emailServer: net.Server;
|
|
|
|
|
let webClient: got.Got;
|
|
|
|
|
let emailClient: nodemailer.Transporter;
|
|
|
|
|
beforeAll(() => {
|
|
|
|
|
const rootDirectory = fs.mkdtempSync(
|
|
|
|
|
path.join(os.tmpdir(), "kill-the-newsletter--test--")
|
|
|
|
|
);
|
|
|
|
|
const { webApplication, emailApplication } = killTheNewsletter(rootDirectory);
|
|
|
|
|
webServer = webApplication.listen(new URL(webApplication.get("url")).port);
|
|
|
|
|
emailServer = emailApplication.listen(
|
|
|
|
|
new URL(webApplication.get("email")).port
|
|
|
|
|
);
|
|
|
|
|
webClient = got.default.extend({ prefixUrl: webApplication.get("url") });
|
|
|
|
|
emailClient = nodemailer.createTransport(webApplication.get("email"));
|
|
|
|
|
});
|
|
|
|
|
afterAll(() => {
|
|
|
|
|
webServer.close();
|
|
|
|
|
emailServer.close();
|
|
|
|
|
});
|
2020-03-18 20:21:44 +01:00
|
|
|
|
|
2020-03-19 02:44:21 +01:00
|
|
|
|
test("create feed", async () => {
|
2020-03-20 20:08:04 +01:00
|
|
|
|
const identifier = await createFeed();
|
2020-07-23 17:11:41 +02:00
|
|
|
|
const feed = await getFeed(identifier);
|
|
|
|
|
const entry = feed.querySelector("feed > entry:first-of-type")!;
|
|
|
|
|
const alternate = await getAlternate(
|
|
|
|
|
entry.querySelector("link")!.getAttribute("href")!
|
|
|
|
|
);
|
|
|
|
|
expect(feed.querySelector("feed > title")!.textContent).toBe("My Feed");
|
|
|
|
|
expect(entry.querySelector("title")!.textContent).toBe(
|
|
|
|
|
"“My Feed” Inbox Created"
|
|
|
|
|
);
|
|
|
|
|
expect(alternate.querySelector("p")!.textContent).toMatch(
|
|
|
|
|
"Sign up for the newsletter with"
|
|
|
|
|
);
|
2020-03-18 20:21:44 +01:00
|
|
|
|
});
|
2020-03-19 01:16:00 +01:00
|
|
|
|
|
2020-03-19 05:20:28 +01:00
|
|
|
|
describe("receive email", () => {
|
2020-03-31 20:14:44 +02:00
|
|
|
|
test("‘updated’ field is updated", async () => {
|
|
|
|
|
const identifier = await createFeed();
|
|
|
|
|
const before = await getFeed(identifier);
|
|
|
|
|
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-04-03 23:06:43 +02:00
|
|
|
|
html: "<p>HTML content</p>",
|
2020-03-31 20:14:44 +02:00
|
|
|
|
});
|
|
|
|
|
const after = await getFeed(identifier);
|
2020-07-23 17:11:41 +02:00
|
|
|
|
expect(after.querySelector("feed > updated")!.textContent).not.toBe(
|
|
|
|
|
before.querySelector("feed > updated")!.textContent
|
2020-03-31 20:14:44 +02:00
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2020-03-19 05:20:28 +01:00
|
|
|
|
test("HTML 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
|
|
|
|
html: "<p>HTML content</p>",
|
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("author > name")!.textContent).toBe(
|
|
|
|
|
"publisher@example.com"
|
|
|
|
|
);
|
|
|
|
|
expect(entry.querySelector("title")!.textContent).toBe("New Message");
|
|
|
|
|
expect(entry.querySelector("content")!.textContent).toMatch("HTML content");
|
|
|
|
|
expect(alternate.querySelector("p")!.textContent).toMatch("HTML content");
|
2020-03-19 05:20:28 +01:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
});
|
2020-03-19 04:36:02 +01:00
|
|
|
|
|
2020-12-22 21:50:37 +01:00
|
|
|
|
test("‘noindex’ header", async () => {
|
|
|
|
|
const identifier = await createFeed();
|
|
|
|
|
const feed = await getFeed(identifier);
|
|
|
|
|
const entry = feed.querySelector("feed > entry:first-of-type")!;
|
|
|
|
|
const alternatePath = entry.querySelector("link")!.getAttribute("href")!;
|
|
|
|
|
expect((await webClient.get(`/`)).headers["x-robots-tag"]).toBeUndefined();
|
|
|
|
|
expect(
|
|
|
|
|
(await webClient.get(`/feeds/${identifier}.xml`)).headers["x-robots-tag"]
|
|
|
|
|
).toBe("noindex");
|
|
|
|
|
expect((await webClient.get(alternatePath)).headers["x-robots-tag"]).toBe(
|
|
|
|
|
"noindex"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2020-03-19 01:16:00 +01:00
|
|
|
|
afterAll(() => {
|
2020-03-20 22:31:46 +01:00
|
|
|
|
webServer.close();
|
2020-03-31 18:57:56 +02:00
|
|
|
|
emailServer.close();
|
2020-03-19 01:16:00 +01:00
|
|
|
|
});
|
2020-03-19 04:36:02 +01:00
|
|
|
|
|
|
|
|
|
async function createFeed(): Promise<string> {
|
2020-07-23 17:11:41 +02:00
|
|
|
|
return JSDOM.fragment(
|
|
|
|
|
(
|
|
|
|
|
await webClient.post(
|
|
|
|
|
"/",
|
|
|
|
|
qs.stringify({
|
|
|
|
|
name: "My Feed",
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
).data
|
|
|
|
|
)
|
|
|
|
|
.querySelector("code")!
|
|
|
|
|
.textContent!.split("@")[0];
|
2020-03-19 04:36:02 +01:00
|
|
|
|
}
|
2020-03-19 05:20:28 +01:00
|
|
|
|
|
2020-07-23 17:11:41 +02:00
|
|
|
|
async function getFeed(identifier: string): Promise<Document> {
|
|
|
|
|
return new JSDOM((await webClient.get(`/feeds/${identifier}.xml`)).data, {
|
|
|
|
|
contentType: "text/xml",
|
|
|
|
|
}).window.document;
|
2020-03-19 05:20:28 +01:00
|
|
|
|
}
|
2020-07-15 21:29:08 +02:00
|
|
|
|
|
2020-07-23 17:11:41 +02:00
|
|
|
|
async function getAlternate(url: string): Promise<DocumentFragment> {
|
|
|
|
|
return JSDOM.fragment((await webClient.get(url)).data);
|
2020-07-15 21:29:08 +02:00
|
|
|
|
}
|