|
|
@@ -0,0 +1,163 @@
|
|
|
+/* @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);
|
|
|
+ });
|
|
|
+ });
|
|
|
+});
|