import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockAuth } = vi.hoisted(() => ({ mockAuth: vi.fn<() => Promise<{ user: { id: string } } | null>>(), })); vi.mock("@/lib/auth", () => ({ auth: mockAuth })); vi.mock("@/server/lib/resolve-workspace", () => ({ resolveWorkspace: vi.fn(), })); import { auth } from "@/lib/auth"; import { resolveWorkspace } from "@/server/lib/resolve-workspace"; import { POST } from "./route"; const publicOrigin = "https://echodo.stillwell.cloud"; function request( origin: string | null, forwardedHost = "echodo.stillwell.cloud", ) { const headers = new Headers({ "Content-Type": "application/json", "x-forwarded-host": forwardedHost, "x-forwarded-proto": "https", }); if (origin !== null) headers.set("origin", origin); return new Request("http://0.0.0.0:3000/api/chat/ticket", { method: "POST", headers, body: JSON.stringify({ workspace: "example" }), }); } beforeEach(() => { vi.stubEnv("NODE_ENV", "production"); vi.stubEnv("NEXTAUTH_URL", publicOrigin); vi.stubEnv("AUTH_URL", ""); vi.stubEnv("CHAT_TICKET_SECRET", "x".repeat(64)); mockAuth.mockResolvedValue({ user: { id: "10000000-0000-4000-8000-000000000001" }, }); vi.mocked(resolveWorkspace).mockResolvedValue({ id: "20000000-0000-4000-8000-000000000001", } as Awaited>); }); afterEach(() => { vi.unstubAllEnvs(); vi.clearAllMocks(); }); describe("Chat ticket origin boundary", () => { it("accepts the configured public origin behind an internal Docker URL", async () => { const response = await POST(request(publicOrigin)); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("no-store"); expect(resolveWorkspace).toHaveBeenCalled(); }); it.each([ null, "null", "https://evil.example", `${publicOrigin}.evil.example`, `${publicOrigin}/path`, ])("rejects origin %s before session lookup", async (origin) => { expect((await POST(request(origin, "evil.example"))).status).toBe(403); expect(auth).not.toHaveBeenCalled(); }); it("does not trust forged forwarded host and protocol", async () => { expect( (await POST(request("https://evil.example", "evil.example"))).status, ).toBe(403); }); it("fails closed without a configured production origin", async () => { vi.stubEnv("NEXTAUTH_URL", ""); expect((await POST(request("http://0.0.0.0:3000"))).status).toBe(403); }); it("fails closed with an invalid configured origin", async () => { vi.stubEnv("NEXTAUTH_URL", "not a URL"); expect((await POST(request(publicOrigin))).status).toBe(403); }); it("still requires an authenticated session", async () => { mockAuth.mockResolvedValue(null); expect((await POST(request(publicOrigin))).status).toBe(401); }); it("does not mint a ticket when workspace authorization fails", async () => { vi.mocked(resolveWorkspace).mockRejectedValue(new Error("denied")); expect((await POST(request(publicOrigin))).status).toBe(404); }); });