trails/apps/journal/app/lib/email.server.test.ts
Ullrich Schäfer 0a8dd0b766
Add transactional emails (SMTP) and planner features (no-go areas, notes, crash recovery)
Transactional emails:
- Add nodemailer SMTP email module with dev-mode console logging
- Magic link template and welcome template with HTML + plain text
- Wire sendMagicLink into login flow, sendWelcome into registration
- Update privacy page and deploy docs for SMTP configuration

Planner features:
- No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs,
  passed to BRouter as nogos parameter, route recomputes on change
- Session notes: collaborative Y.Text textarea in sidebar tab
- Crash recovery: periodic localStorage save of Yjs state, restore on reconnect
- Rate limit session creation (10/IP/hour) in /new route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:00:42 +01:00

65 lines
2.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock nodemailer before importing
vi.mock("nodemailer", () => ({
createTransport: vi.fn().mockReturnValue({
sendMail: vi.fn().mockResolvedValue({ messageId: "test-id" }),
}),
}));
describe("email.server", () => {
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
process.env.NODE_ENV = originalEnv;
delete process.env.SMTP_URL;
});
it("logs to console in dev mode", async () => {
process.env.NODE_ENV = "development";
const { sendEmail } = await import("./email.server");
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await sendEmail("test@example.com", "Test Subject", "<p>Hello</p>", "Hello");
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("test@example.com"),
);
consoleSpy.mockRestore();
});
it("does not call SMTP in dev mode", async () => {
process.env.NODE_ENV = "development";
const nodemailer = await import("nodemailer");
const { sendEmail } = await import("./email.server");
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await sendEmail("test@example.com", "Test", "<p>Hi</p>", "Hi");
expect(nodemailer.createTransport).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
it("magicLinkTemplate includes link and expiry note", async () => {
const { magicLinkTemplate } = await import("./email.server");
const { html, text } = magicLinkTemplate("https://trails.cool/auth/verify?token=abc");
expect(html).toContain("https://trails.cool/auth/verify?token=abc");
expect(html).toContain("15 minutes");
expect(text).toContain("https://trails.cool/auth/verify?token=abc");
expect(text).toContain("15 minutes");
});
it("welcomeTemplate includes username", async () => {
const { welcomeTemplate } = await import("./email.server");
const { html, text } = welcomeTemplate("Alice");
expect(html).toContain("Alice");
expect(text).toContain("Alice");
expect(html).toContain("trails.cool");
});
});