/* @vitest-environment node */ import { describe, it, expect } from "vitest"; import { LOGIN_REASONS, isKnownLoginReason, sanitizeNext, buildLoginUrl, parseLoginParams, } from "./authRedirect.js"; describe("lib/frontend/authRedirect", () => { describe("isKnownLoginReason", () => { it("accepts only the explicit known reasons", () => { expect(isKnownLoginReason(LOGIN_REASONS.EXPIRED)).toBe(true); expect(isKnownLoginReason(LOGIN_REASONS.LOGGED_OUT)).toBe(true); expect(isKnownLoginReason("something-else")).toBe(false); expect(isKnownLoginReason("")).toBe(false); expect(isKnownLoginReason(null)).toBe(false); expect(isKnownLoginReason(undefined)).toBe(false); }); }); describe("sanitizeNext", () => { it("accepts internal paths that start with a single slash", () => { expect(sanitizeNext("/")).toBe("/"); expect(sanitizeNext("/NL01")).toBe("/NL01"); expect(sanitizeNext("/NL01/2025/12/31")).toBe("/NL01/2025/12/31"); expect(sanitizeNext("/NL01?x=1#hash")).toBe("/NL01?x=1#hash"); }); it("trims whitespace", () => { expect(sanitizeNext(" /NL01 ")).toBe("/NL01"); }); it("rejects empty or non-string input", () => { expect(sanitizeNext("")).toBe(null); expect(sanitizeNext(" ")).toBe(null); expect(sanitizeNext(null)).toBe(null); expect(sanitizeNext(undefined)).toBe(null); expect(sanitizeNext(123)).toBe(null); }); it("rejects paths that do not start with '/'", () => { expect(sanitizeNext("NL01")).toBe(null); expect(sanitizeNext("login")).toBe(null); expect(sanitizeNext("http://evil.com")).toBe(null); }); it("rejects protocol-relative URLs and backslashes", () => { expect(sanitizeNext("//evil.com")).toBe(null); // Backslashes can lead to confusing path interpretations. expect(sanitizeNext("/\\evil")).toBe(null); expect(sanitizeNext("/NL01\\2025")).toBe(null); }); }); describe("buildLoginUrl", () => { it("returns plain /login when no params are provided", () => { expect(buildLoginUrl()).toBe("/login"); expect(buildLoginUrl({})).toBe("/login"); }); it("adds reason when valid", () => { expect(buildLoginUrl({ reason: "expired" })).toBe( "/login?reason=expired" ); }); it("adds next when valid", () => { expect(buildLoginUrl({ next: "/NL01" })).toBe("/login?next=%2FNL01"); }); it("adds reason first, then next (stable ordering)", () => { expect(buildLoginUrl({ reason: "expired", next: "/NL01" })).toBe( "/login?reason=expired&next=%2FNL01" ); }); it("ignores invalid reason and unsafe next", () => { // Invalid reason is dropped; unsafe next is dropped. expect(buildLoginUrl({ reason: "nope", next: "//evil.com" })).toBe( "/login" ); }); }); describe("parseLoginParams", () => { it("parses from URLSearchParams (client-side style)", () => { const sp = new URLSearchParams({ reason: "expired", next: "/NL01/2025", }); expect(parseLoginParams(sp)).toEqual({ reason: "expired", next: "/NL01/2025", }); }); it("parses from plain object (Next.js server searchParams style)", () => { const sp = { reason: "logged-out", next: "/NL02", }; expect(parseLoginParams(sp)).toEqual({ reason: "logged-out", next: "/NL02", }); }); it("normalizes unknown reason to null and sanitizes next", () => { const sp = new URLSearchParams({ reason: "unknown", next: "//evil.com", }); expect(parseLoginParams(sp)).toEqual({ reason: null, next: null, }); }); it("handles missing params", () => { const sp = new URLSearchParams(); expect(parseLoginParams(sp)).toEqual({ reason: null, next: null }); }); }); });