import { beforeEach, describe, expect, it, vi } from "vitest"; import { NextRequest } from "next/server"; const mockGetAuthenticatedUser = vi.fn(); const mockStartCase = vi.fn(); const mockGetUser = vi.fn(); const mockGetConfig = vi.fn(); vi.mock("@/lib/supabase/server.js", () => ({ getAuthenticatedUser: () => mockGetAuthenticatedUser(), })); vi.mock("@/lib/graph/orchestrator.js", () => ({ startCase: (...args) => mockStartCase(...args), })); vi.mock("@/lib/config", () => ({ getConfig: () => mockGetConfig(), })); vi.mock("@supabase/ssr", () => ({ createServerClient: () => ({ auth: { getUser: () => mockGetUser(), exchangeCodeForSession: vi.fn().mockResolvedValue(undefined), }, }), })); describe("auth callback redirect origin", () => { beforeEach(() => { vi.clearAllMocks(); }); it("uses forwarded host/proto for redirect when behind proxy", async () => { const { GET } = await import("@/app/auth/callback/route.js"); const request = new Request("http://0.0.0.0:3000/auth/callback?code=abc123", { headers: { "x-forwarded-host": "confidence.rdbcloud.co.uk", "x-forwarded-proto": "https", }, }); const response = await GET(request); expect(response.status).toBe(307); expect(response.headers.get("location")).toBe("https://confidence.rdbcloud.co.uk/"); }); it("falls back to request origin when no forwarded headers", async () => { const { GET } = await import("@/app/auth/callback/route.js"); const request = new Request("http://localhost:3000/auth/callback?code=xyz"); const response = await GET(request); expect(response.status).toBe(307); expect(response.headers.get("location")).toBe("http://localhost:3000/"); }); }); describe("authenticated product boundary", () => { beforeEach(() => { vi.clearAllMocks(); }); it("rejects an unauthenticated protected API request", async () => { mockGetAuthenticatedUser.mockResolvedValue(null); const { withAuthenticatedApi } = await import("@/lib/supabase/api-auth.js"); const handler = vi.fn(); const response = await withAuthenticatedApi(handler)(new Request("http://localhost/api/cases/start")); expect(response.status).toBe(401); expect(handler).not.toHaveBeenCalled(); }); it("allows an authenticated protected API request to reach existing route behavior", async () => { mockGetAuthenticatedUser.mockResolvedValue({ id: "user-1" }); mockStartCase.mockResolvedValue({ success: true, updatedSituationGraph: {} }); const { POST } = await import("@/app/api/cases/start/route.js"); const response = await POST(new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({ scenario: "A scenario" }), })); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ success: true }); expect(mockStartCase).toHaveBeenCalledWith({ scenario: "A scenario" }); }); it("supplies the auth callback as the magic-link redirect target", async () => { const { magicLinkRedirectTo } = await import("@/lib/supabase/browser.js"); expect(magicLinkRedirectTo("http://localhost:3000")).toBe("http://localhost:3000/auth/callback"); }); it("keeps infrastructure health public and does not leak config details", async () => { mockGetConfig.mockReturnValue({ ok: true, config: {} }); const { GET } = await import("@/app/api/health/route.js"); const response = await GET(); expect(mockGetAuthenticatedUser).not.toHaveBeenCalled(); const body = await response.json(); expect(body).toHaveProperty("healthy"); // Must not expose private config details in the public response expect(JSON.stringify(body)).not.toContain("baseUrl"); expect(JSON.stringify(body)).not.toContain("model"); expect(JSON.stringify(body)).not.toContain("ollama"); }); it("reports unhealthy generic state when config is missing", async () => { mockGetConfig.mockReturnValue({ ok: false }); const { GET } = await import("@/app/api/health/route.js"); const response = await GET(); expect(response.status).toBe(500); const body = await response.json(); expect(body).toEqual({ healthy: false }); }); it("does not convert /api/health to 401 via middleware when unauthenticated", async () => { mockGetUser.mockResolvedValue({ data: { user: null } }); const { middleware } = await import("@/middleware.js"); const response = await middleware(new NextRequest("http://localhost:3000/api/health")); expect(response.status).toBe(200); }); it("redirects unauthenticated product access to the login surface", async () => { mockGetUser.mockResolvedValue({ data: { user: null } }); const { middleware } = await import("@/middleware.js"); const response = await middleware(new NextRequest("http://localhost:3000/")); expect(response.status).toBe(307); expect(response.headers.get("location")).toBe("http://localhost:3000/login?next=%2F"); }); });