/* @vitest-environment node */ import { describe, it, expect } from "vitest"; import { MUST_CHANGE_PASSWORD_GATE_PARAM, MUST_CHANGE_PASSWORD_GATE_VALUE, isProfilePath, sanitizeMustChangePasswordNext, shouldRedirectToProfileForPasswordChange, buildMustChangePasswordRedirectUrl, parseMustChangePasswordGateParams, resolveMustChangePasswordResumePath, } from "./mustChangePasswordGate.js"; describe("lib/frontend/auth/mustChangePasswordGate", () => { describe("isProfilePath", () => { it("returns true for /profile and profile subpaths", () => { expect(isProfilePath("/profile")).toBe(true); expect(isProfilePath("/profile/security")).toBe(true); }); it("returns false for non-profile routes", () => { expect(isProfilePath("/")).toBe(false); expect(isProfilePath("/NL01")).toBe(false); expect(isProfilePath("/NL01/search")).toBe(false); }); }); describe("sanitizeMustChangePasswordNext", () => { it("accepts safe internal non-profile targets", () => { expect(sanitizeMustChangePasswordNext("/NL01")).toBe("/NL01"); expect(sanitizeMustChangePasswordNext("/NL01/search?scope=all")).toBe( "/NL01/search?scope=all" ); }); it("rejects unsafe or profile targets", () => { expect(sanitizeMustChangePasswordNext("//evil.com")).toBe(null); expect(sanitizeMustChangePasswordNext("https://evil.com")).toBe(null); expect(sanitizeMustChangePasswordNext("/profile")).toBe(null); expect(sanitizeMustChangePasswordNext("/profile?x=1")).toBe(null); }); }); describe("shouldRedirectToProfileForPasswordChange", () => { it("forces redirect for non-profile routes while flag is true", () => { expect( shouldRedirectToProfileForPasswordChange({ pathname: "/NL01", mustChangePassword: true, }) ).toBe(true); }); it("does not force redirect when already on profile or flag is false", () => { expect( shouldRedirectToProfileForPasswordChange({ pathname: "/profile", mustChangePassword: true, }) ).toBe(false); expect( shouldRedirectToProfileForPasswordChange({ pathname: "/NL01", mustChangePassword: false, }) ).toBe(false); }); }); describe("buildMustChangePasswordRedirectUrl", () => { it("builds profile URL with marker and safe next", () => { expect(buildMustChangePasswordRedirectUrl("/NL01/search")).toBe( "/profile?mustChangePasswordGate=1&next=%2FNL01%2Fsearch" ); }); it("always includes gate marker and drops invalid next", () => { expect(buildMustChangePasswordRedirectUrl("//evil.com")).toBe( `/profile?${MUST_CHANGE_PASSWORD_GATE_PARAM}=${MUST_CHANGE_PASSWORD_GATE_VALUE}` ); }); }); describe("parseMustChangePasswordGateParams", () => { it("parses marker and next from URLSearchParams", () => { const sp = new URLSearchParams({ mustChangePasswordGate: "1", next: "/NL01", }); expect(parseMustChangePasswordGateParams(sp)).toEqual({ isGateMarker: true, next: "/NL01", }); }); it("normalizes invalid input", () => { const sp = new URLSearchParams({ mustChangePasswordGate: "yes", next: "/profile", }); expect(parseMustChangePasswordGateParams(sp)).toEqual({ isGateMarker: false, next: null, }); }); }); describe("resolveMustChangePasswordResumePath", () => { it("returns next only when marker is set, route is profile, and flag is false", () => { const sp = new URLSearchParams({ mustChangePasswordGate: "1", next: "/NL01/search", }); expect( resolveMustChangePasswordResumePath({ pathname: "/profile", searchParams: sp, mustChangePassword: false, }) ).toBe("/NL01/search"); }); it("returns null when any resume condition is not satisfied", () => { const sp = new URLSearchParams({ mustChangePasswordGate: "1", next: "/NL01/search", }); expect( resolveMustChangePasswordResumePath({ pathname: "/profile", searchParams: sp, mustChangePassword: true, }) ).toBe(null); expect( resolveMustChangePasswordResumePath({ pathname: "/NL01", searchParams: sp, mustChangePassword: false, }) ).toBe(null); expect( resolveMustChangePasswordResumePath({ pathname: "/profile", searchParams: new URLSearchParams({ mustChangePasswordGate: "0", next: "/NL01/search", }), mustChangePassword: false, }) ).toBe(null); }); }); });