import { expect, test } from "bun:test"; import { createContext } from "@wrnexus/core"; import { getOriginalRequestMethod, getOriginalRequestOrigin, getOriginalRequestPath, getOriginalRequestUrl, redirectToLogin, } from "../src/index.ts"; function context(url: string, headers: HeadersInit = {}) { const parsed = new URL(url); return createContext(new Request(parsed, { headers }), parsed); } test("uses the direct context URL when no gateway headers exist", () => { const ctx = context("https://app.example.test/account?tab=security"); expect(getOriginalRequestUrl(ctx).href).toBe("https://app.example.test/account?tab=security"); expect(getOriginalRequestOrigin(ctx)).toBe("https://app.example.test"); expect(getOriginalRequestPath(ctx)).toBe("/account?tab=security"); expect(getOriginalRequestMethod(ctx)).toBe("GET"); }); test("reconstructs an allowed original gateway URL", () => { const ctx = context("http://sso.localhost:3000/api/verify", { "x-forwarded-host": "admin.localhost:3000", "x-forwarded-proto": "http", "x-original-method": "GET", "x-original-uri": "/users?page=2", }); const url = getOriginalRequestUrl(ctx, { allowedHosts: ["admin.localhost:3000"] }); expect(url.href).toBe("http://admin.localhost:3000/users?page=2"); expect(getOriginalRequestMethod(ctx)).toBe("GET"); }); test("keeps the protected app URL when the SSO verifier crosses the gateway again", () => { const ctx = context("http://sso.localhost:3000/api/verify", { "x-forwarded-host": "sso.localhost:3000", "x-forwarded-proto": "http", "x-original-host": "admin.localhost:3000", "x-original-proto": "http", "x-original-method": "GET", "x-original-uri": "/settings?tab=security", }); const response = redirectToLogin(ctx, "/login", { allowedHosts: ["admin.localhost:3000"], }); const location = new URL(response.headers.get("location")!); expect(location.origin).toBe("http://sso.localhost:3000"); expect(location.pathname).toBe("/login"); expect(location.searchParams.get("returnTo")).toBe( "http://admin.localhost:3000/settings?tab=security", ); }); test("rejects untrusted hosts and unsafe request paths", () => { const untrusted = context("http://sso.localhost/api/verify", { "x-forwarded-host": "evil.example", "x-original-uri": "/", }); const unsafePath = context("http://sso.localhost/api/verify", { "x-forwarded-host": "admin.localhost", "x-original-uri": "//evil.example/steal", }); expect(() => getOriginalRequestUrl(untrusted)).toThrow("Untrusted forwarded host"); expect(() => getOriginalRequestUrl(unsafePath, { allowedHosts: ["admin.localhost"] })).toThrow( "absolute request path", ); }); test("creates a safe login redirect with an encoded returnTo URL", () => { const ctx = context("http://sso.localhost:3000/api/verify", { "x-forwarded-host": "admin.localhost:3000", "x-forwarded-proto": "http", "x-original-uri": "/reports?range=week", }); const response = redirectToLogin(ctx, "/login", { allowedHosts: new Set(["admin.localhost:3000"]), }); const location = new URL(response.headers.get("location")!); expect(response.status).toBe(302); expect(location.origin).toBe("http://sso.localhost:3000"); expect(location.pathname).toBe("/login"); expect(location.searchParams.get("returnTo")).toBe( "http://admin.localhost:3000/reports?range=week", ); }); test("relative login redirects use the configured public app origin", () => { const previous = process.env.WRNEXUS_APP_ORIGIN; process.env.WRNEXUS_APP_ORIGIN = "https://sso.example.test"; try { const ctx = context("http://127.0.0.1:3002/api/verify", { "x-original-host": "admin.example.test", "x-original-proto": "https", "x-original-uri": "/settings", }); const response = redirectToLogin(ctx, "/sign-in", { allowedHosts: ["admin.example.test"], }); const location = new URL(response.headers.get("location")!); expect(location.origin).toBe("https://sso.example.test"); expect(location.pathname).toBe("/sign-in"); expect(location.searchParams.get("returnTo")).toBe("https://admin.example.test/settings"); } finally { if (previous === undefined) delete process.env.WRNEXUS_APP_ORIGIN; else process.env.WRNEXUS_APP_ORIGIN = previous; } }); test("supports an allowed-host callback and custom response options", () => { const ctx = context("https://login.example.test/api/verify", { "x-forwarded-host": "reports.example.test", "x-forwarded-proto": "https", "x-original-uri": "/", }); const response = redirectToLogin(ctx, "https://login.example.test/sign-in?tenant=acme", { allowedHosts: (host) => host.endsWith(".example.test"), returnToParam: "next", status: 303, }); const location = new URL(response.headers.get("location")!); expect(response.status).toBe(303); expect(location.searchParams.get("tenant")).toBe("acme"); expect(location.searchParams.get("next")).toBe("https://reports.example.test/"); });