// ABOUTME: Unit tests for current cycle phase API route. // ABOUTME: Tests GET /api/cycle/current for cycle day and phase information. import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { User } from "@/types"; // Module-level variable to control mock user in tests let currentMockUser: User | null = null; // Create mock PocketBase getOne function that returns fresh user data const mockPbGetOne = vi.fn().mockImplementation(() => { if (!currentMockUser) { throw new Error("User not found"); } return Promise.resolve({ id: currentMockUser.id, email: currentMockUser.email, lastPeriodDate: currentMockUser.lastPeriodDate?.toISOString(), cycleLength: currentMockUser.cycleLength, }); }); // Create mock PocketBase client const mockPb = { collection: vi.fn(() => ({ getOne: mockPbGetOne, })), }; // Mock PocketBase client for database operations vi.mock("@/lib/pocketbase", () => ({ createPocketBaseClient: vi.fn(() => mockPb), loadAuthFromCookies: vi.fn(), isAuthenticated: vi.fn(() => currentMockUser !== null), getCurrentUser: vi.fn(() => currentMockUser), })); // Mock the auth-middleware module vi.mock("@/lib/auth-middleware", () => ({ withAuth: vi.fn((handler) => { return async (request: NextRequest) => { if (!currentMockUser) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } return handler(request, currentMockUser, mockPb); }; }), })); import { GET } from "./route"; describe("GET /api/cycle/current", () => { const createMockUser = (overrides: Partial = {}): User => ({ id: "user123", email: "test@example.com", garminConnected: false, garminOauth1Token: "", garminOauth2Token: "", garminTokenExpiresAt: new Date("2025-06-01"), garminRefreshTokenExpiresAt: null, calendarToken: "cal-secret-token", lastPeriodDate: new Date("2025-01-01"), cycleLength: 31, notificationTime: "07:00", timezone: "America/New_York", activeOverrides: [], created: new Date("2024-01-01"), updated: new Date("2025-01-10"), ...overrides, }); const mockRequest = {} as NextRequest; beforeEach(() => { vi.clearAllMocks(); currentMockUser = null; // Mock current date to 2025-01-10 for predictable testing vi.useFakeTimers(); vi.setSystemTime(new Date("2025-01-10T12:00:00Z")); }); afterEach(() => { vi.useRealTimers(); }); it("returns 401 when not authenticated", async () => { currentMockUser = null; const response = await GET(mockRequest); expect(response.status).toBe(401); const body = await response.json(); expect(body.error).toBe("Unauthorized"); }); it("returns 400 when user has no lastPeriodDate", async () => { currentMockUser = createMockUser({ lastPeriodDate: null as unknown as Date, }); const response = await GET(mockRequest); expect(response.status).toBe(400); const body = await response.json(); expect(body.error).toContain("lastPeriodDate"); }); it("returns current cycle day and phase information", async () => { // lastPeriodDate: 2025-01-01, current: 2025-01-10 // That's 9 days difference, so cycle day = 10 currentMockUser = createMockUser({ lastPeriodDate: new Date("2025-01-01"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.cycleDay).toBe(10); expect(body.phase).toBe("FOLLICULAR"); }); it("returns complete phase configuration", async () => { currentMockUser = createMockUser({ lastPeriodDate: new Date("2025-01-01"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.phaseConfig).toBeDefined(); expect(body.phaseConfig.name).toBe("FOLLICULAR"); expect(body.phaseConfig.weeklyLimit).toBe(120); expect(body.phaseConfig.trainingType).toBe("Strength + rebounding"); // Phase configs days are for reference; actual boundaries are calculated dynamically expect(body.phaseConfig.days).toEqual([4, 15]); expect(body.phaseConfig.dailyAvg).toBe(17); }); it("calculates daysUntilNextPhase correctly", async () => { // Cycle day 10, in FOLLICULAR (days 4-15 for 31-day cycle) // Days until OVULATION starts (day 16): 16 - 10 = 6 currentMockUser = createMockUser({ lastPeriodDate: new Date("2025-01-01"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.daysUntilNextPhase).toBe(6); }); it("returns correct data for MENSTRUAL phase", async () => { // Set lastPeriodDate to 2025-01-08 so cycle day = 3 on 2025-01-10 currentMockUser = createMockUser({ lastPeriodDate: new Date("2025-01-08"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.cycleDay).toBe(3); expect(body.phase).toBe("MENSTRUAL"); expect(body.phaseConfig.weeklyLimit).toBe(30); expect(body.daysUntilNextPhase).toBe(1); // Day 4 is FOLLICULAR }); it("returns correct data for OVULATION phase", async () => { // For 31-day cycle, OVULATION is days 16-17 // Set lastPeriodDate so cycle day = 16 (start of OVULATION) // If current is 2025-01-10, need lastPeriodDate = 2024-12-26 (15 days ago) currentMockUser = createMockUser({ lastPeriodDate: new Date("2024-12-26"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.cycleDay).toBe(16); expect(body.phase).toBe("OVULATION"); expect(body.phaseConfig.weeklyLimit).toBe(80); expect(body.daysUntilNextPhase).toBe(2); // Day 18 is EARLY_LUTEAL }); it("returns correct data for LATE_LUTEAL phase", async () => { // Set lastPeriodDate so cycle day = 28 (in LATE_LUTEAL days 25-31) // If current is 2025-01-10, need lastPeriodDate = 2024-12-14 (27 days ago) currentMockUser = createMockUser({ lastPeriodDate: new Date("2024-12-14"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.cycleDay).toBe(28); expect(body.phase).toBe("LATE_LUTEAL"); expect(body.phaseConfig.weeklyLimit).toBe(50); // Days until next cycle starts (new MENSTRUAL): 31 - 28 + 1 = 4 expect(body.daysUntilNextPhase).toBe(4); }); it("handles cycle rollover correctly", async () => { // Set lastPeriodDate so we're past day 31 with cycleLength 31 // 40 days ago: (40 % 31) + 1 = 10 currentMockUser = createMockUser({ lastPeriodDate: new Date("2024-12-01"), cycleLength: 31, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); // 40 days: (40 % 31) + 1 = 10 expect(body.cycleDay).toBe(10); expect(body.phase).toBe("FOLLICULAR"); }); it("works with custom cycle length", async () => { // With 28 day cycle, same offset gives different results // lastPeriodDate: 2025-01-01, current: 2025-01-10 // 9 days difference, so cycle day = 10 (within 28) currentMockUser = createMockUser({ lastPeriodDate: new Date("2025-01-01"), cycleLength: 28, }); const response = await GET(mockRequest); expect(response.status).toBe(200); const body = await response.json(); expect(body.cycleDay).toBe(10); expect(body.cycleLength).toBe(28); }); });