// ABOUTME: Unit tests for email sending utilities. // ABOUTME: Tests email composition, subject lines, and Resend integration. import { afterEach, describe, expect, it, vi } from "vitest"; const { mockSend } = vi.hoisted(() => ({ mockSend: vi.fn().mockResolvedValue({ id: "mock-email-id" }), })); // Mock the resend module before importing email utilities vi.mock("resend", () => ({ Resend: class MockResend { emails = { send: mockSend }; }, })); import type { DailyEmailData } from "./email"; import { sendDailyEmail, sendPeriodConfirmationEmail } from "./email"; describe("sendDailyEmail", () => { afterEach(() => { vi.clearAllMocks(); }); const sampleData: DailyEmailData = { to: "user@example.com", cycleDay: 15, phase: "OVULATION", decision: { status: "TRAIN", reason: "Body battery high, HRV balanced - great day for training!", icon: "💪", }, bodyBatteryCurrent: 85, bodyBatteryYesterdayLow: 45, hrvStatus: "Balanced", weekIntensity: 60, phaseLimit: 80, remainingMinutes: 20, seeds: "Sesame (1-2 tbsp) + Sunflower (1-2 tbsp)", carbRange: "100-150g", ketoGuidance: "No - exit keto, need carbs for ovulation", }; it("sends email with correct subject line", async () => { await sendDailyEmail(sampleData); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ subject: "Today's Training: 💪 TRAIN", }), ); }); it("includes cycle day and phase in email body", async () => { await sendDailyEmail(sampleData); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("📅 CYCLE DAY: 15 (OVULATION)"); }); it("includes decision icon and reason", async () => { await sendDailyEmail(sampleData); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain( "💪 Body battery high, HRV balanced - great day for training!", ); }); it("includes biometric data in email body", async () => { await sendDailyEmail(sampleData); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("Body Battery Now: 85"); expect(call.text).toContain("Yesterday's Low: 45"); expect(call.text).toContain("HRV Status: Balanced"); expect(call.text).toContain("Week Intensity: 60 / 80 minutes"); expect(call.text).toContain("Remaining: 20 minutes"); }); it("includes nutrition guidance in email body", async () => { await sendDailyEmail(sampleData); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain( "🌱 SEEDS: Sesame (1-2 tbsp) + Sunflower (1-2 tbsp)", ); expect(call.text).toContain("🍽️ MACROS: 100-150g"); expect(call.text).toContain( "🥑 KETO: No - exit keto, need carbs for ovulation", ); }); it("handles null body battery values with N/A", async () => { const dataWithNulls: DailyEmailData = { ...sampleData, bodyBatteryCurrent: null, bodyBatteryYesterdayLow: null, }; await sendDailyEmail(dataWithNulls); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("Body Battery Now: N/A"); expect(call.text).toContain("Yesterday's Low: N/A"); }); it("sends email to correct recipient", async () => { await sendDailyEmail(sampleData); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ to: "user@example.com", }), ); }); it("includes auto-generated footer", async () => { await sendDailyEmail(sampleData); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("Auto-generated by PhaseFlow"); }); }); describe("sendPeriodConfirmationEmail", () => { afterEach(() => { vi.clearAllMocks(); }); it("sends email with correct subject", async () => { await sendPeriodConfirmationEmail( "user@example.com", new Date("2025-01-15"), 31, ); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ subject: "🔵 Period Tracking Updated", }), ); }); it("includes last period date in body", async () => { await sendPeriodConfirmationEmail( "user@example.com", new Date("2025-01-15"), 31, ); const call = mockSend.mock.calls[0][0]; // Date formatting depends on locale, so check for key parts expect(call.text).toContain("Your cycle has been reset"); expect(call.text).toContain("Last period:"); }); it("includes cycle length in body", async () => { await sendPeriodConfirmationEmail( "user@example.com", new Date("2025-01-15"), 28, ); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("Phase calendar updated for next 28 days"); }); it("sends to correct recipient", async () => { await sendPeriodConfirmationEmail( "test@example.com", new Date("2025-01-15"), 31, ); expect(mockSend).toHaveBeenCalledWith( expect.objectContaining({ to: "test@example.com", }), ); }); it("includes auto-generated footer", async () => { await sendPeriodConfirmationEmail( "user@example.com", new Date("2025-01-15"), 31, ); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain("Auto-generated by PhaseFlow"); }); it("mentions calendar update timing", async () => { await sendPeriodConfirmationEmail( "user@example.com", new Date("2025-01-15"), 31, ); const call = mockSend.mock.calls[0][0]; expect(call.text).toContain( "Your calendar will update automatically within 24 hours", ); }); });