authRedirect.test.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. /* @vitest-environment node */
  2. import { describe, it, expect } from "vitest";
  3. import {
  4. LOGIN_REASONS,
  5. isKnownLoginReason,
  6. sanitizeNext,
  7. buildLoginUrl,
  8. parseLoginParams,
  9. } from "./authRedirect.js";
  10. describe("lib/frontend/authRedirect", () => {
  11. describe("isKnownLoginReason", () => {
  12. it("accepts only the explicit known reasons", () => {
  13. expect(isKnownLoginReason(LOGIN_REASONS.EXPIRED)).toBe(true);
  14. expect(isKnownLoginReason(LOGIN_REASONS.LOGGED_OUT)).toBe(true);
  15. expect(isKnownLoginReason("something-else")).toBe(false);
  16. expect(isKnownLoginReason("")).toBe(false);
  17. expect(isKnownLoginReason(null)).toBe(false);
  18. expect(isKnownLoginReason(undefined)).toBe(false);
  19. });
  20. });
  21. describe("sanitizeNext", () => {
  22. it("accepts internal paths that start with a single slash", () => {
  23. expect(sanitizeNext("/")).toBe("/");
  24. expect(sanitizeNext("/NL01")).toBe("/NL01");
  25. expect(sanitizeNext("/NL01/2025/12/31")).toBe("/NL01/2025/12/31");
  26. expect(sanitizeNext("/NL01?x=1#hash")).toBe("/NL01?x=1#hash");
  27. });
  28. it("trims whitespace", () => {
  29. expect(sanitizeNext(" /NL01 ")).toBe("/NL01");
  30. });
  31. it("rejects empty or non-string input", () => {
  32. expect(sanitizeNext("")).toBe(null);
  33. expect(sanitizeNext(" ")).toBe(null);
  34. expect(sanitizeNext(null)).toBe(null);
  35. expect(sanitizeNext(undefined)).toBe(null);
  36. expect(sanitizeNext(123)).toBe(null);
  37. });
  38. it("rejects paths that do not start with '/'", () => {
  39. expect(sanitizeNext("NL01")).toBe(null);
  40. expect(sanitizeNext("login")).toBe(null);
  41. expect(sanitizeNext("http://evil.com")).toBe(null);
  42. });
  43. it("rejects protocol-relative URLs and backslashes", () => {
  44. expect(sanitizeNext("//evil.com")).toBe(null);
  45. // Backslashes can lead to confusing path interpretations.
  46. expect(sanitizeNext("/\\evil")).toBe(null);
  47. expect(sanitizeNext("/NL01\\2025")).toBe(null);
  48. });
  49. });
  50. describe("buildLoginUrl", () => {
  51. it("returns plain /login when no params are provided", () => {
  52. expect(buildLoginUrl()).toBe("/login");
  53. expect(buildLoginUrl({})).toBe("/login");
  54. });
  55. it("adds reason when valid", () => {
  56. expect(buildLoginUrl({ reason: "expired" })).toBe(
  57. "/login?reason=expired"
  58. );
  59. });
  60. it("adds next when valid", () => {
  61. expect(buildLoginUrl({ next: "/NL01" })).toBe("/login?next=%2FNL01");
  62. });
  63. it("adds reason first, then next (stable ordering)", () => {
  64. expect(buildLoginUrl({ reason: "expired", next: "/NL01" })).toBe(
  65. "/login?reason=expired&next=%2FNL01"
  66. );
  67. });
  68. it("ignores invalid reason and unsafe next", () => {
  69. // Invalid reason is dropped; unsafe next is dropped.
  70. expect(buildLoginUrl({ reason: "nope", next: "//evil.com" })).toBe(
  71. "/login"
  72. );
  73. });
  74. });
  75. describe("parseLoginParams", () => {
  76. it("parses from URLSearchParams (client-side style)", () => {
  77. const sp = new URLSearchParams({
  78. reason: "expired",
  79. next: "/NL01/2025",
  80. });
  81. expect(parseLoginParams(sp)).toEqual({
  82. reason: "expired",
  83. next: "/NL01/2025",
  84. });
  85. });
  86. it("parses from plain object (Next.js server searchParams style)", () => {
  87. const sp = {
  88. reason: "logged-out",
  89. next: "/NL02",
  90. };
  91. expect(parseLoginParams(sp)).toEqual({
  92. reason: "logged-out",
  93. next: "/NL02",
  94. });
  95. });
  96. it("normalizes unknown reason to null and sanitizes next", () => {
  97. const sp = new URLSearchParams({
  98. reason: "unknown",
  99. next: "//evil.com",
  100. });
  101. expect(parseLoginParams(sp)).toEqual({
  102. reason: null,
  103. next: null,
  104. });
  105. });
  106. it("handles missing params", () => {
  107. const sp = new URLSearchParams();
  108. expect(parseLoginParams(sp)).toEqual({ reason: null, next: null });
  109. });
  110. });
  111. });