Introduces a shared email module that wraps all transactional emails in a cohesive dark-themed template matching the site's visual identity (Barlow font, #14171e background, #adf661 CTA buttons, logomark PNG header). - app/lib/email.server.ts: new module with sendEmail, wrapInEmailTemplate, emailButton, emailParagraph, and escapeHtml helpers; Resend instance is lazily initialized as a singleton - public/email-logo.png: 96×124px PNG of the logomark for email use (SVG gradients are not supported in Outlook) - auth.server.ts: password reset and email verification now use the branded template; errors are thrown so Better Auth can surface failures - support.tsx: internal notification uses sendEmail; adds a user-facing branded confirmation email (fire-and-forget so failures don't surface to the user); plain text fallback included - scripts/sync-prod-db.sh: remove first_name/last_name from sanitize query (columns were dropped in #399) - 13 unit tests covering all template helpers Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import { describe, expect, it, beforeEach, afterEach } from "vitest";
|
|
import {
|
|
escapeHtml,
|
|
emailParagraph,
|
|
emailButton,
|
|
wrapInEmailTemplate,
|
|
} from "~/lib/email.server";
|
|
|
|
describe("escapeHtml", () => {
|
|
it("escapes all five special characters", () => {
|
|
expect(escapeHtml('&<>"\'')).toBe("&<>"'");
|
|
});
|
|
|
|
it("leaves safe strings unchanged", () => {
|
|
expect(escapeHtml("Hello World")).toBe("Hello World");
|
|
});
|
|
});
|
|
|
|
describe("emailParagraph", () => {
|
|
it("wraps content in a <p> tag", () => {
|
|
const result = emailParagraph("Hello");
|
|
expect(result).toContain("<p ");
|
|
expect(result).toContain("Hello");
|
|
expect(result).toContain("</p>");
|
|
});
|
|
|
|
it("passes HTML through unescaped (caller is responsible for escaping)", () => {
|
|
const result = emailParagraph('<a href="x">link</a>');
|
|
expect(result).toContain('<a href="x">');
|
|
});
|
|
});
|
|
|
|
describe("emailButton", () => {
|
|
it("contains the href and button text", () => {
|
|
const result = emailButton("https://example.com/verify", "Verify My Email");
|
|
expect(result).toContain("https://example.com/verify");
|
|
expect(result).toContain("Verify My Email");
|
|
});
|
|
|
|
it("renders a table-wrapped anchor for Outlook compatibility", () => {
|
|
const result = emailButton("https://example.com", "Click me");
|
|
expect(result).toContain("<table");
|
|
expect(result).toContain("<a ");
|
|
expect(result).toContain('href="https://example.com"');
|
|
});
|
|
});
|
|
|
|
describe("wrapInEmailTemplate", () => {
|
|
const originalAppUrl = process.env.APP_URL;
|
|
|
|
beforeEach(() => {
|
|
process.env.APP_URL = "https://test.brackt.com";
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalAppUrl === undefined) {
|
|
delete process.env.APP_URL;
|
|
} else {
|
|
process.env.APP_URL = originalAppUrl;
|
|
}
|
|
});
|
|
|
|
it("returns a complete HTML document", () => {
|
|
const result = wrapInEmailTemplate("<p>Body</p>");
|
|
expect(result).toContain("<!DOCTYPE html>");
|
|
expect(result).toContain("<html");
|
|
expect(result).toContain("</html>");
|
|
});
|
|
|
|
it("injects the content into the template", () => {
|
|
const result = wrapInEmailTemplate("<p>My content</p>");
|
|
expect(result).toContain("<p>My content</p>");
|
|
});
|
|
|
|
it("includes the preheader text when provided", () => {
|
|
const result = wrapInEmailTemplate("<p>Body</p>", "Preview text here");
|
|
expect(result).toContain("Preview text here");
|
|
});
|
|
|
|
it("omits the preheader span when not provided", () => {
|
|
const result = wrapInEmailTemplate("<p>Body</p>");
|
|
expect(result).not.toContain("mso-hide:all");
|
|
});
|
|
|
|
it("uses APP_URL for the logo src", () => {
|
|
const result = wrapInEmailTemplate("<p>Body</p>");
|
|
expect(result).toContain("https://test.brackt.com/email-logo.png");
|
|
});
|
|
|
|
it("falls back to brackt.com when APP_URL is not set", () => {
|
|
delete process.env.APP_URL;
|
|
const result = wrapInEmailTemplate("<p>Body</p>");
|
|
expect(result).toContain("https://brackt.com/email-logo.png");
|
|
});
|
|
|
|
it("escapes preheader text to prevent injection", () => {
|
|
const result = wrapInEmailTemplate("<p>Body</p>", "<script>alert(1)</script>");
|
|
expect(result).not.toContain("<script>");
|
|
expect(result).toContain("<script>");
|
|
});
|
|
});
|