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>
65 lines
2.1 KiB
TypeScript
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");
|
|
});
|
|
});
|