diff --git a/app/lib/__tests__/email.server.test.ts b/app/lib/__tests__/email.server.test.ts
new file mode 100644
index 0000000..b926951
--- /dev/null
+++ b/app/lib/__tests__/email.server.test.ts
@@ -0,0 +1,101 @@
+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
tag", () => {
+ const result = emailParagraph("Hello");
+ expect(result).toContain("
");
+ });
+
+ it("passes HTML through unescaped (caller is responsible for escaping)", () => {
+ const result = emailParagraph('link');
+ expect(result).toContain('');
+ });
+});
+
+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("
{
+ 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("Body
");
+ expect(result).toContain("");
+ expect(result).toContain("");
+ });
+
+ it("injects the content into the template", () => {
+ const result = wrapInEmailTemplate("My content
");
+ expect(result).toContain("My content
");
+ });
+
+ it("includes the preheader text when provided", () => {
+ const result = wrapInEmailTemplate("Body
", "Preview text here");
+ expect(result).toContain("Preview text here");
+ });
+
+ it("omits the preheader span when not provided", () => {
+ const result = wrapInEmailTemplate("Body
");
+ expect(result).not.toContain("mso-hide:all");
+ });
+
+ it("uses APP_URL for the logo src", () => {
+ const result = wrapInEmailTemplate("Body
");
+ 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("Body
");
+ expect(result).toContain("https://brackt.com/email-logo.png");
+ });
+
+ it("escapes preheader text to prevent injection", () => {
+ const result = wrapInEmailTemplate("Body
", "");
+ expect(result).not.toContain("