@auth0/nextjs-auth0
Version:
Auth0 Next.js SDK
500 lines (499 loc) • 24.7 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { deleteChunkedCookie, getChunkedCookie, setChunkedCookie } from "./cookies.js";
// Create mock implementation for RequestCookies and ResponseCookies
const createMocks = () => {
const cookieStore = new Map();
const reqCookies = {
get: vi.fn((...args) => {
const name = typeof args[0] === "string" ? args[0] : args[0].name;
if (cookieStore.has(name)) {
return { name, value: cookieStore.get(name) };
}
return undefined;
}),
getAll: vi.fn((...args) => {
if (args.length === 0) {
return Array.from(cookieStore.entries()).map(([name, value]) => ({
name,
value
}));
}
const name = typeof args[0] === "string" ? args[0] : args[0].name;
return cookieStore.has(name)
? [{ name, value: cookieStore.get(name) }]
: [];
}),
has: vi.fn((name) => cookieStore.has(name)),
set: vi.fn((...args) => {
const name = typeof args[0] === "string" ? args[0] : args[0].name;
const value = typeof args[0] === "string" ? args[1] : args[0].value;
cookieStore.set(name, value);
return reqCookies;
}),
delete: vi.fn((names) => {
if (Array.isArray(names)) {
return names.map((name) => cookieStore.delete(name));
}
return cookieStore.delete(names);
}),
clear: vi.fn(() => {
cookieStore.clear();
return reqCookies;
}),
get size() {
return cookieStore.size;
},
[Symbol.iterator]: vi.fn(() => cookieStore.entries())
};
const resCookies = {
get: vi.fn((...args) => {
const name = typeof args[0] === "string" ? args[0] : args[0].name;
if (cookieStore.has(name)) {
return { name, value: cookieStore.get(name) };
}
return undefined;
}),
getAll: vi.fn((...args) => {
if (args.length === 0) {
return Array.from(cookieStore.entries()).map(([name, value]) => ({
name,
value
}));
}
const name = typeof args[0] === "string" ? args[0] : args[0].name;
return cookieStore.has(name)
? [{ name, value: cookieStore.get(name) }]
: [];
}),
has: vi.fn((name) => cookieStore.has(name)),
set: vi.fn((...args) => {
const name = typeof args[0] === "string" ? args[0] : args[0].name;
const value = typeof args[0] === "string" ? args[1] : args[0].value;
cookieStore.set(name, value);
return resCookies;
}),
delete: vi.fn((...args) => {
const name = typeof args[0] === "string" ? args[0] : args[0].name;
cookieStore.delete(name);
return resCookies;
}),
toString: vi.fn(() => {
return Array.from(cookieStore.entries())
.map(([name, value]) => `${name}=${value}`)
.join("; ");
})
};
return { reqCookies, resCookies, cookieStore };
};
describe("Chunked Cookie Utils", () => {
let reqCookies;
let resCookies;
let cookieStore;
beforeEach(() => {
const mocks = createMocks();
reqCookies = mocks.reqCookies;
resCookies = mocks.resCookies;
cookieStore = mocks.cookieStore;
// Spy on console.warn
vi.spyOn(console, "warn").mockImplementation(() => { });
});
afterEach(() => {
vi.clearAllMocks();
});
describe("setChunkedCookie", () => {
it("should set a single cookie when value is small enough", () => {
const name = "testCookie";
const value = "small value";
const options = { path: "/" };
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
expect(reqCookies.set).toHaveBeenCalledTimes(1);
expect(reqCookies.set).toHaveBeenCalledWith(name, value);
});
it("should split cookie into chunks when value exceeds max size", () => {
const name = "largeCookie";
const options = { path: "/" };
// Create a large string (8000 bytes)
const largeValue = "a".repeat(8000);
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
// Should create 3 chunks (8000 / 3500 ≈ 2.3, rounded up to 3)
// called 4 times:
// 3 calls to set the chunks
// 1 call to remove the non-chunked cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(reqCookies.set).toHaveBeenCalledTimes(3);
// Check first chunk
expect(resCookies.set).toHaveBeenCalledWith(`${name}__0`, largeValue.slice(0, 3500), options);
// Check second chunk
expect(resCookies.set).toHaveBeenCalledWith(`${name}__1`, largeValue.slice(3500, 7000), options);
// Check third chunk
expect(resCookies.set).toHaveBeenCalledWith(`${name}__2`, largeValue.slice(7000), options);
// Check removal of non-chunked cookie
expect(resCookies.set).toHaveBeenCalledWith(name, "", {
maxAge: 0,
path: "/"
});
});
it("should clear existing chunked cookies when setting a single cookie", () => {
const name = "testCookie";
const value = "small value";
const options = { path: "/" };
const chunk0 = "chunk0 value";
const chunk1 = "chunk1 value";
const chunk2 = "chunk2 value";
cookieStore.set(`${name}__1`, chunk1);
cookieStore.set(`${name}__0`, chunk0);
cookieStore.set(`${name}__2`, chunk2);
setChunkedCookie(name, value, options, reqCookies, resCookies);
// delete the 3 chunked cookies set above and then set the new cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenNthCalledWith(1, name, value, options);
expect(resCookies.set).toHaveBeenNthCalledWith(2, `${name}__1`, "", {
maxAge: 0,
path: "/"
});
expect(resCookies.set).toHaveBeenNthCalledWith(3, `${name}__0`, "", {
maxAge: 0,
path: "/"
});
expect(resCookies.set).toHaveBeenNthCalledWith(4, `${name}__2`, "", {
maxAge: 0,
path: "/"
});
expect(reqCookies.set).toHaveBeenCalledTimes(1);
expect(reqCookies.set).toHaveBeenCalledWith(name, value);
expect(reqCookies.delete).toHaveBeenCalledTimes(3);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__0`);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__1`);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__2`);
});
it("should clear existing single cookies when setting a chunked cookie", () => {
const name = "testCookie";
const value = "small value";
cookieStore.set(`${name}`, value);
// Create a large string (8000 bytes)
const largeValue = "a".repeat(8000);
const options = { path: "/" };
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
expect(reqCookies.delete).toHaveBeenCalledTimes(1);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}`);
// set a chunked cookie with 3 chunks and delete the existing single cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenNthCalledWith(1, `${name}__0`, largeValue.slice(0, 3500), options);
expect(resCookies.set).toHaveBeenNthCalledWith(2, `${name}__1`, largeValue.slice(3500, 7000), options);
expect(resCookies.set).toHaveBeenNthCalledWith(3, `${name}__2`, largeValue.slice(7000), options);
expect(resCookies.set).toHaveBeenNthCalledWith(4, name, "", {
maxAge: 0,
path: "/"
});
expect(reqCookies.set).toHaveBeenCalledTimes(3);
});
it("should clean up unused chunks when cookie shrinks", () => {
const name = "testCookie";
const options = { path: "/" };
const chunk0 = "chunk0 value";
const chunk1 = "chunk1 value";
const chunk2 = "chunk2 value";
const chunk3 = "chunk3 value";
const chunk4 = "chunk4 value";
cookieStore.set(`${name}__1`, chunk1);
cookieStore.set(`${name}__0`, chunk0);
cookieStore.set(`${name}__2`, chunk2);
cookieStore.set(`${name}__3`, chunk3);
cookieStore.set(`${name}__4`, chunk4);
const largeValue = "a".repeat(8000);
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
// It is called 3 times.
// 2 times for the chunks
// 1 time for the non chunked cookie
expect(reqCookies.delete).toHaveBeenCalledTimes(3);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__3`);
expect(reqCookies.delete).toHaveBeenCalledWith(`${name}__4`);
expect(reqCookies.delete).toHaveBeenCalledWith(name);
});
// New tests for domain and transient options
it("should set the domain property for a single cookie", () => {
const name = "domainCookie";
const value = "small value";
const options = {
path: "/",
domain: "example.com",
httpOnly: true,
secure: true,
sameSite: "lax"
};
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expect.objectContaining({ domain: "example.com" }));
});
it("should set the domain property for chunked cookies", () => {
const name = "largeDomainCookie";
const largeValue = "a".repeat(8000);
const options = {
path: "/",
domain: "example.com",
httpOnly: true,
secure: true,
sameSite: "lax"
};
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
// called 4 times:
// 3 calls to set the chunks
// 1 call to remove the non-chunked cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenNthCalledWith(1, `${name}__0`, expect.any(String), expect.objectContaining({ domain: "example.com" }));
expect(resCookies.set).toHaveBeenNthCalledWith(2, `${name}__1`, expect.any(String), expect.objectContaining({ domain: "example.com" }));
expect(resCookies.set).toHaveBeenNthCalledWith(3, `${name}__2`, expect.any(String), expect.objectContaining({ domain: "example.com" }));
expect(resCookies.set).toHaveBeenNthCalledWith(4, name, "", {
domain: "example.com",
maxAge: 0,
path: "/"
});
});
it("should omit maxAge for a single transient cookie", () => {
const name = "transientCookie";
const value = "small value";
const options = {
path: "/",
maxAge: 3600,
transient: true,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.maxAge; // maxAge should be removed
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
expect(resCookies.set).not.toHaveBeenCalledWith(name, value, expect.objectContaining({ maxAge: 3600 }));
});
it("should omit maxAge for chunked transient cookies", () => {
const name = "largeTransientCookie";
const largeValue = "a".repeat(8000);
const options = {
path: "/",
maxAge: 3600,
transient: true,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.maxAge; // maxAge should be removed
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
// called 4 times:
// 3 calls to set the chunks
// 1 call to remove the non-chunked cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenNthCalledWith(1, `${name}__0`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(2, `${name}__1`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(3, `${name}__2`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(4, name, "", {
maxAge: 0,
path: "/"
});
expect(resCookies.set).not.toHaveBeenCalledWith(expect.any(String), expect.any(String), expect.objectContaining({ maxAge: 3600 }));
});
it("should include maxAge for a single non-transient cookie", () => {
const name = "nonTransientCookie";
const value = "small value";
const options = {
path: "/",
maxAge: 3600,
transient: false,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expectedOptions);
expect(resCookies.set).toHaveBeenCalledWith(name, value, expect.objectContaining({ maxAge: 3600 }));
});
it("should include maxAge for chunked non-transient cookies", () => {
const name = "largeNonTransientCookie";
const largeValue = "a".repeat(8000);
const options = {
path: "/",
maxAge: 3600,
transient: false,
httpOnly: true,
secure: true,
sameSite: "lax"
};
const expectedOptions = { ...options };
delete expectedOptions.transient; // transient flag itself is not part of the cookie options
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
// called 4 times:
// 3 calls to set the chunks
// 1 call to remove the non-chunked cookie
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenNthCalledWith(1, `${name}__0`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(2, `${name}__1`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(3, `${name}__2`, expect.any(String), expectedOptions);
expect(resCookies.set).toHaveBeenNthCalledWith(4, name, "", {
maxAge: 0,
path: "/"
});
});
describe("getChunkedCookie", () => {
it("should return undefined if no cookie or chunks are found", () => {
const result = getChunkedCookie("nonexistent", reqCookies, false);
expect(result).toBeUndefined();
});
it("should retrieve a single non-chunked cookie", () => {
const name = "singleCookie";
const value = "single value";
cookieStore.set(name, value);
const result = getChunkedCookie(name, reqCookies, false);
expect(result).toBe(value);
expect(reqCookies.get).toHaveBeenCalledWith(name);
});
it("should retrieve and combine chunked cookies", () => {
const name = "chunkedCookie";
const chunk0 = "chunk0 value";
const chunk1 = "chunk1 value";
const chunk2 = "chunk2 value";
// Set in reverse order to test sorting
cookieStore.set(`${name}__1`, chunk1);
cookieStore.set(`${name}__0`, chunk0);
cookieStore.set(`${name}__2`, chunk2);
expect(getChunkedCookie(name, reqCookies, false)).toBe(`${chunk0}${chunk1}${chunk2}`);
});
it("should retrieve and combine chunked cookies using legacy format", () => {
const name = "legacyChunkedCookie";
const chunk0 = "legacy chunk0 value";
const chunk1 = "legacy chunk1 value";
// Set in reverse order to test sorting
cookieStore.set(`${name}.1`, chunk1);
cookieStore.set(`${name}.0`, chunk0);
expect(getChunkedCookie(name, reqCookies, true)).toBe(`${chunk0}${chunk1}`);
});
it("should return undefined when chunks are not in a complete sequence", () => {
const name = "incompleteCookie";
// Add incomplete chunks (missing chunk1)
cookieStore.set(`${name}__0`, "chunk0");
cookieStore.set(`${name}__2`, "chunk2");
const result = getChunkedCookie(name, reqCookies, false);
expect(result).toBeUndefined();
expect(console.warn).toHaveBeenCalled();
});
});
describe("deleteChunkedCookie", () => {
it("should delete the regular cookie", () => {
const name = "regularCookie";
cookieStore.set(name, "regular value");
deleteChunkedCookie(name, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledWith(name, "", {
maxAge: 0
});
});
it("should delete all chunks of a cookie", () => {
const name = "chunkedCookie";
// Add chunks
cookieStore.set(`${name}__0`, "chunk0");
cookieStore.set(`${name}__1`, "chunk1");
cookieStore.set(`${name}__2`, "chunk2");
// Add unrelated cookie
cookieStore.set("otherCookie", "other value");
deleteChunkedCookie(name, reqCookies, resCookies);
// Should delete main cookie and 3 chunks
expect(resCookies.set).toHaveBeenCalledTimes(4);
expect(resCookies.set).toHaveBeenCalledWith(name, "", {
maxAge: 0
});
expect(resCookies.set).toHaveBeenCalledWith(`${name}__0`, "", {
maxAge: 0
});
expect(resCookies.set).toHaveBeenCalledWith(`${name}__1`, "", {
maxAge: 0
});
expect(resCookies.set).toHaveBeenCalledWith(`${name}__2`, "", {
maxAge: 0
});
// Should not delete unrelated cookies
expect(resCookies.set).not.toHaveBeenCalledWith("otherCookie", "", {
maxAge: 0
});
});
});
describe("Edge Cases", () => {
it("should handle empty values correctly", () => {
const name = "emptyCookie";
const value = "";
const options = { path: "/" };
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
});
it("should handle values at the exact chunk boundary", () => {
const name = "boundaryValueCookie";
const value = "a".repeat(3500); // Exactly MAX_CHUNK_SIZE
const options = { path: "/" };
setChunkedCookie(name, value, options, reqCookies, resCookies);
// Should still fit in one cookie
expect(resCookies.set).toHaveBeenCalledTimes(1);
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
});
it("should handle special characters in cookie values", () => {
const name = "specialCharCookie";
const value = '{"special":"characters","with":"quotation marks","and":"😀 emoji"}';
const options = { path: "/" };
setChunkedCookie(name, value, options, reqCookies, resCookies);
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
// Setup for retrieval
cookieStore.set(name, value);
const result = getChunkedCookie(name, reqCookies);
expect(result).toBe(value);
});
it("should handle multi-byte characters correctly", () => {
const name = "multiByteCookie";
// Create a test string with multi-byte characters (emojis)
const value = "Hello 😀 world 🌍 with emojis 🎉";
const options = { path: "/" };
// Store the cookie
setChunkedCookie(name, value, options, reqCookies, resCookies);
// For the retrieval test, manually set up the cookies
// We're testing the retrieval functionality, not the chunking itself
cookieStore.clear();
cookieStore.set(name, value);
// Verify retrieval works correctly with multi-byte characters
const result = getChunkedCookie(name, reqCookies);
expect(result).toBe(value);
// Verify emoji characters were preserved
expect(result).toContain("😀");
expect(result).toContain("🌍");
expect(result).toContain("🎉");
});
it("should handle very large cookies properly", () => {
const name = "veryLargeCookie";
const value = "a".repeat(10000); // Will create multiple chunks
const options = { path: "/" };
setChunkedCookie(name, value, options, reqCookies, resCookies);
// Get chunks count (10000 / 3500 ≈ 2.86, so we need 3 chunks)
const expectedChunks = Math.ceil(10000 / 3500);
// called 4 times:
// 3 calls to set the chunks
// 1 call to remove the non-chunked cookie
expect(resCookies.set).toHaveBeenCalledTimes(expectedChunks + 1);
// Clear and set up cookies for retrieval test
cookieStore.clear();
// Setup for getChunkedCookie retrieval
for (let i = 0; i < expectedChunks; i++) {
const start = i * 3500;
const end = Math.min((i + 1) * 3500, 10000);
cookieStore.set(`${name}__${i}`, value.slice(start, end));
}
const result = getChunkedCookie(name, reqCookies);
expect(result).toBe(value);
expect(result.length).toBe(10000);
});
});
});
});