| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- /* @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 });
- });
- });
- });
|