mustChangePasswordGate.test.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. /* @vitest-environment node */
  2. import { describe, it, expect } from "vitest";
  3. import {
  4. MUST_CHANGE_PASSWORD_GATE_PARAM,
  5. MUST_CHANGE_PASSWORD_GATE_VALUE,
  6. isProfilePath,
  7. sanitizeMustChangePasswordNext,
  8. shouldRedirectToProfileForPasswordChange,
  9. buildMustChangePasswordRedirectUrl,
  10. parseMustChangePasswordGateParams,
  11. resolveMustChangePasswordResumePath,
  12. } from "./mustChangePasswordGate.js";
  13. describe("lib/frontend/auth/mustChangePasswordGate", () => {
  14. describe("isProfilePath", () => {
  15. it("returns true for /profile and profile subpaths", () => {
  16. expect(isProfilePath("/profile")).toBe(true);
  17. expect(isProfilePath("/profile/security")).toBe(true);
  18. });
  19. it("returns false for non-profile routes", () => {
  20. expect(isProfilePath("/")).toBe(false);
  21. expect(isProfilePath("/NL01")).toBe(false);
  22. expect(isProfilePath("/NL01/search")).toBe(false);
  23. });
  24. });
  25. describe("sanitizeMustChangePasswordNext", () => {
  26. it("accepts safe internal non-profile targets", () => {
  27. expect(sanitizeMustChangePasswordNext("/NL01")).toBe("/NL01");
  28. expect(sanitizeMustChangePasswordNext("/NL01/search?scope=all")).toBe(
  29. "/NL01/search?scope=all"
  30. );
  31. });
  32. it("rejects unsafe or profile targets", () => {
  33. expect(sanitizeMustChangePasswordNext("//evil.com")).toBe(null);
  34. expect(sanitizeMustChangePasswordNext("https://evil.com")).toBe(null);
  35. expect(sanitizeMustChangePasswordNext("/profile")).toBe(null);
  36. expect(sanitizeMustChangePasswordNext("/profile?x=1")).toBe(null);
  37. });
  38. });
  39. describe("shouldRedirectToProfileForPasswordChange", () => {
  40. it("forces redirect for non-profile routes while flag is true", () => {
  41. expect(
  42. shouldRedirectToProfileForPasswordChange({
  43. pathname: "/NL01",
  44. mustChangePassword: true,
  45. })
  46. ).toBe(true);
  47. });
  48. it("does not force redirect when already on profile or flag is false", () => {
  49. expect(
  50. shouldRedirectToProfileForPasswordChange({
  51. pathname: "/profile",
  52. mustChangePassword: true,
  53. })
  54. ).toBe(false);
  55. expect(
  56. shouldRedirectToProfileForPasswordChange({
  57. pathname: "/NL01",
  58. mustChangePassword: false,
  59. })
  60. ).toBe(false);
  61. });
  62. });
  63. describe("buildMustChangePasswordRedirectUrl", () => {
  64. it("builds profile URL with marker and safe next", () => {
  65. expect(buildMustChangePasswordRedirectUrl("/NL01/search")).toBe(
  66. "/profile?mustChangePasswordGate=1&next=%2FNL01%2Fsearch"
  67. );
  68. });
  69. it("always includes gate marker and drops invalid next", () => {
  70. expect(buildMustChangePasswordRedirectUrl("//evil.com")).toBe(
  71. `/profile?${MUST_CHANGE_PASSWORD_GATE_PARAM}=${MUST_CHANGE_PASSWORD_GATE_VALUE}`
  72. );
  73. });
  74. });
  75. describe("parseMustChangePasswordGateParams", () => {
  76. it("parses marker and next from URLSearchParams", () => {
  77. const sp = new URLSearchParams({
  78. mustChangePasswordGate: "1",
  79. next: "/NL01",
  80. });
  81. expect(parseMustChangePasswordGateParams(sp)).toEqual({
  82. isGateMarker: true,
  83. next: "/NL01",
  84. });
  85. });
  86. it("normalizes invalid input", () => {
  87. const sp = new URLSearchParams({
  88. mustChangePasswordGate: "yes",
  89. next: "/profile",
  90. });
  91. expect(parseMustChangePasswordGateParams(sp)).toEqual({
  92. isGateMarker: false,
  93. next: null,
  94. });
  95. });
  96. });
  97. describe("resolveMustChangePasswordResumePath", () => {
  98. it("returns next only when marker is set, route is profile, and flag is false", () => {
  99. const sp = new URLSearchParams({
  100. mustChangePasswordGate: "1",
  101. next: "/NL01/search",
  102. });
  103. expect(
  104. resolveMustChangePasswordResumePath({
  105. pathname: "/profile",
  106. searchParams: sp,
  107. mustChangePassword: false,
  108. })
  109. ).toBe("/NL01/search");
  110. });
  111. it("returns null when any resume condition is not satisfied", () => {
  112. const sp = new URLSearchParams({
  113. mustChangePasswordGate: "1",
  114. next: "/NL01/search",
  115. });
  116. expect(
  117. resolveMustChangePasswordResumePath({
  118. pathname: "/profile",
  119. searchParams: sp,
  120. mustChangePassword: true,
  121. })
  122. ).toBe(null);
  123. expect(
  124. resolveMustChangePasswordResumePath({
  125. pathname: "/NL01",
  126. searchParams: sp,
  127. mustChangePassword: false,
  128. })
  129. ).toBe(null);
  130. expect(
  131. resolveMustChangePasswordResumePath({
  132. pathname: "/profile",
  133. searchParams: new URLSearchParams({
  134. mustChangePasswordGate: "0",
  135. next: "/NL01/search",
  136. }),
  137. mustChangePassword: false,
  138. })
  139. ).toBe(null);
  140. });
  141. });
  142. });