Staging was delivering magic links that looked expired because the token query string was encoded and wrapped in plaintext MIME. Also read rate-limit retry from the API error body, mention spam in the success copy, and document SES relay DNS. Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import nodemailer from "nodemailer";
|
|
import {
|
|
buildVerifyLinkParts,
|
|
resolveMailFrom,
|
|
} from "../../lib/server/mail";
|
|
|
|
const VERIFY_URL =
|
|
"https://staging.communityrule.info/api/auth/magic-link/verify?token=5IdE_BHowaw-QJj7Rwue7CbB8wDXvYITvnxRb1FGqxA";
|
|
|
|
function decodeQuotedPrintable(value: string): string {
|
|
return value
|
|
.replace(/=\r?\n/g, "")
|
|
.replace(/=([0-9A-Fa-f]{2})/g, (_, hex: string) =>
|
|
String.fromCharCode(Number.parseInt(hex, 16)),
|
|
);
|
|
}
|
|
|
|
const MAIL_FROM_KEYS = ["SMTP_FROM", "CLOUDRON_MAIL_FROM"] as const;
|
|
const ORIGINAL_FROM = Object.fromEntries(
|
|
MAIL_FROM_KEYS.map((key) => [key, process.env[key]]),
|
|
) as Record<(typeof MAIL_FROM_KEYS)[number], string | undefined>;
|
|
|
|
afterEach(() => {
|
|
for (const key of MAIL_FROM_KEYS) {
|
|
const original = ORIGINAL_FROM[key];
|
|
if (original === undefined) delete process.env[key];
|
|
else process.env[key] = original;
|
|
}
|
|
});
|
|
|
|
describe("buildVerifyLinkParts", () => {
|
|
it("puts the exact verify URL in both text and the HTML href", () => {
|
|
const { text, html } = buildVerifyLinkParts(
|
|
VERIFY_URL,
|
|
"Open this link to sign in (it expires in 15 minutes):",
|
|
"If you did not request this, you can ignore this email.",
|
|
"Sign in",
|
|
);
|
|
expect(text).toContain(VERIFY_URL);
|
|
expect(html).toContain(`href="${VERIFY_URL}"`);
|
|
expect(html).toContain(">Sign in</a>");
|
|
});
|
|
|
|
it("escapes HTML in the intro and href", () => {
|
|
const { html } = buildVerifyLinkParts(
|
|
'https://example.test/verify?token=a&b="c"',
|
|
'View "Rule <beta>"',
|
|
"Ignore if unexpected.",
|
|
"Open",
|
|
);
|
|
expect(html).toContain("View "Rule <beta>"");
|
|
expect(html).toContain(
|
|
'href="https://example.test/verify?token=a&b="c""',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("MIME encoding of verify-link mail", () => {
|
|
it("keeps a clickable href after quoted-printable encoding", async () => {
|
|
const { text, html } = buildVerifyLinkParts(
|
|
VERIFY_URL,
|
|
"Open this link to sign in (it expires in 15 minutes):",
|
|
"If you did not request this, you can ignore this email.",
|
|
"Sign in",
|
|
);
|
|
const transporter = nodemailer.createTransport({
|
|
streamTransport: true,
|
|
buffer: true,
|
|
newline: "unix",
|
|
});
|
|
const info = await transporter.sendMail({
|
|
from: "Community Rule <staging.app@communityrule.info>",
|
|
to: "member@example.com",
|
|
subject: "Sign in to Community Rule",
|
|
text,
|
|
html,
|
|
});
|
|
const raw = Buffer.isBuffer(info.message)
|
|
? info.message.toString("utf8")
|
|
: String(info.message);
|
|
const decoded = decodeQuotedPrintable(raw);
|
|
expect(decoded).toContain(`href="${VERIFY_URL}"`);
|
|
expect(decoded).toContain(VERIFY_URL);
|
|
expect(decoded).not.toContain("token=3D");
|
|
});
|
|
});
|
|
|
|
describe("resolveMailFrom", () => {
|
|
it("prefers SMTP_FROM, then CLOUDRON_MAIL_FROM", () => {
|
|
delete process.env.SMTP_FROM;
|
|
delete process.env.CLOUDRON_MAIL_FROM;
|
|
expect(resolveMailFrom()).toBe("noreply@localhost");
|
|
|
|
process.env.CLOUDRON_MAIL_FROM = "staging.app@communityrule.info";
|
|
expect(resolveMailFrom()).toBe("staging.app@communityrule.info");
|
|
|
|
process.env.SMTP_FROM = "Community Rule <hello@communityrule.info>";
|
|
expect(resolveMailFrom()).toBe(
|
|
"Community Rule <hello@communityrule.info>",
|
|
);
|
|
});
|
|
});
|