141 lines
4.9 KiB
JavaScript
141 lines
4.9 KiB
JavaScript
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");
|
|
});
|
|
}); |