const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const test = require("node:test"); const vm = require("node:vm"); const rootDir = path.resolve(__dirname, "..", ".."); const normalize = (value) => JSON.parse(JSON.stringify(value)); const loadLocalCookieCompatibility = () => { const filePath = path.join(rootDir, "lib", "cookies.js"); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( 'import * as cookie from "cookie";', 'const cookie = require("cookie");' ); source = source.replace( 'import setCookieParser from "set-cookie-parser";', 'const setCookieParser = require("set-cookie-parser");' ); source = source.replace(/export const\s+/g, "const "); source += "\nmodule.exports = { parseCookies, setCookie, destroyCookie };\n"; const context = { module: { exports: {} }, exports: {}, require, console, document: global.document, window: global.window }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadCookieImplementation = () => { return loadLocalCookieCompatibility(); }; const withBrowserDocument = async (initialCookies, callback) => { const previousDocument = global.document; const previousWindow = global.window; const cookieJar = new Map(Object.entries(initialCookies)); const writes = []; const documentMock = {}; Object.defineProperty(documentMock, "cookie", { configurable: true, get() { return Array.from(cookieJar.entries()) .map(([name, value]) => `${name}=${value}`) .join("; "); }, set(serializedCookie) { writes.push(serializedCookie); const [nameValue, ...attributes] = serializedCookie.split(";"); const separatorIndex = nameValue.indexOf("="); const name = nameValue.slice(0, separatorIndex); const value = nameValue.slice(separatorIndex + 1); const maxAge = attributes .map((attribute) => attribute.trim()) .find((attribute) => attribute.toLowerCase().startsWith("max-age=") ); if (maxAge && Number(maxAge.split("=")[1]) < 0) { cookieJar.delete(name); return; } cookieJar.set(name, value); } }); global.window = {}; global.document = documentMock; try { await callback({ cookieJar, writes }); } finally { if (previousWindow === undefined) { delete global.window; } else { global.window = previousWindow; } if (previousDocument === undefined) { delete global.document; } else { global.document = previousDocument; } } }; const createResponse = (initialHeader) => { let setCookieHeader = initialHeader; const calls = []; return { response: { finished: false, getHeader(name) { assert.strictEqual(name, "Set-Cookie"); return setCookieHeader; }, setHeader(name, value) { calls.push({ name, value }); setCookieHeader = value; } }, calls, getSetCookieHeader: () => setCookieHeader }; }; test("browser parsing preserves characterized nookies behavior", async () => { await withBrowserDocument( { first: "one", encoded: "two%20words", equals: "a%3Db", spaces: "hello%20world", special: "%21%40%23%24%25%5E%26*%28%29", malformed: "%E0%A4%A" }, () => { const { parseCookies } = loadCookieImplementation(); assert.deepStrictEqual(parseCookies(), { first: "one", encoded: "two words", equals: "a=b", spaces: "hello world", special: "!@#$%^&*()", malformed: "%E0%A4%A" }); } ); await withBrowserDocument({}, () => { const { parseCookies } = loadCookieImplementation(); assert.deepStrictEqual(parseCookies(), {}); }); }); test("parseCookies returns an empty object without a browser document", () => { const { parseCookies } = loadCookieImplementation(); assert.deepStrictEqual(normalize(parseCookies()), {}); assert.deepStrictEqual(normalize(parseCookies(null)), {}); assert.deepStrictEqual(normalize(parseCookies({})), {}); }); test("browser writes preserve encoding and supported options", async () => { await withBrowserDocument({}, ({ writes }) => { const { setCookie } = loadCookieImplementation(); const expires = new Date("2030-01-02T03:04:05.000Z"); assert.deepStrictEqual( normalize(setCookie(null, "basic", "value")), {} ); setCookie(null, "pathCookie", "value", { path: "/" }); setCookie(null, "expiresCookie", "value", { expires }); setCookie(null, "maxAgeCookie", "value", { maxAge: 60 }); setCookie(null, "encodedCookie", "hello world=a&b"); assert.deepStrictEqual(writes, [ "basic=value", "pathCookie=value; Path=/", "expiresCookie=value; Expires=Wed, 02 Jan 2030 03:04:05 GMT", "maxAgeCookie=value; Max-Age=60", "encodedCookie=hello%20world%3Da%26b" ]); }); }); test("browser writes reject httpOnly cookies", async () => { await withBrowserDocument({}, () => { const { setCookie } = loadCookieImplementation(); assert.throws( () => setCookie(null, "protected", "value", { httpOnly: true }), /Can not set a httpOnly cookie in the browser\./ ); }); }); test("browser deletion preserves path and uses an empty value with negative maxAge", async () => { await withBrowserDocument( { pedwWatchCase: "case-1", pinsUser: "contact-1", pedw_locale: "cy" }, ({ cookieJar, writes }) => { const { destroyCookie } = loadCookieImplementation(); for (const name of ["pedwWatchCase", "pinsUser", "pedw_locale"]) { assert.deepStrictEqual( normalize(destroyCookie(null, name, { path: "/" })), {} ); } assert.deepStrictEqual(writes, [ "pedwWatchCase=; Max-Age=-1; Path=/", "pinsUser=; Max-Age=-1; Path=/", "pedw_locale=; Max-Age=-1; Path=/" ]); assert.strictEqual(cookieJar.has("pedwWatchCase"), false); assert.strictEqual(cookieJar.has("pinsUser"), false); assert.strictEqual(cookieJar.has("pedw_locale"), false); } ); }); test("server parsing uses ctx.req.headers.cookie", () => { const { parseCookies } = loadCookieImplementation(); assert.deepStrictEqual( parseCookies({ req: { headers: { cookie: "first=one; encoded=two%20words; equals=a%3Db" } } }), { first: "one", encoded: "two words", equals: "a=b" } ); }); test("server writes a Set-Cookie header and preserves unrelated cookies", () => { const { setCookie } = loadCookieImplementation(); const responseState = createResponse([ "unrelated=keep; Path=/; SameSite=Lax", "other=value; HttpOnly; Path=/; SameSite=Strict" ]); assert.deepStrictEqual( normalize( setCookie( { res: responseState.response }, "target", "new value", { path: "/" } ) ), {} ); assert.deepStrictEqual(normalize(responseState.getSetCookieHeader()), [ "unrelated=keep; Path=/; SameSite=Lax", "other=value; Path=/; HttpOnly; SameSite=Strict", "target=new%20value; Path=/" ]); assert.strictEqual(responseState.calls.length, 1); }); test("server deduplicates an exactly matching cookie and keeps cookies with changed values or paths", () => { const { setCookie } = loadCookieImplementation(); const responseState = createResponse([ "target=new; Path=/; SameSite=Lax", "target=old; Path=/; SameSite=Lax", "target=keep-a; Path=/a; SameSite=Lax", "target=keep-b; Path=/b; SameSite=Lax" ]); setCookie({ res: responseState.response }, "target", "new", { path: "/", sameSite: "lax" }); assert.deepStrictEqual(normalize(responseState.getSetCookieHeader()), [ "target=old; Path=/; SameSite=Lax", "target=keep-a; Path=/a; SameSite=Lax", "target=keep-b; Path=/b; SameSite=Lax", "target=new; Path=/; SameSite=Lax" ]); }); test("server does not write after the response has finished", () => { const { setCookie } = loadCookieImplementation(); const responseState = createResponse(); responseState.response.finished = true; const warnings = []; const originalWarn = console.warn; console.warn = (message) => warnings.push(message); try { assert.deepStrictEqual( normalize( setCookie( { res: responseState.response }, "target", "value" ) ), {} ); } finally { console.warn = originalWarn; } assert.deepStrictEqual(responseState.calls, []); assert.deepStrictEqual(warnings, [ 'Not setting "target" cookie. Response has finished.', "You should set cookie before res.send()" ]); }); test("server no-op behavior is preserved for null, empty, and undefined contexts", () => { const { setCookie, destroyCookie } = loadCookieImplementation(); assert.deepStrictEqual(normalize(setCookie(null, "target", "value")), {}); assert.deepStrictEqual(normalize(setCookie({}, "target", "value")), {}); assert.deepStrictEqual( normalize(setCookie(undefined, "target", "value")), {} ); assert.deepStrictEqual( normalize(destroyCookie({}, "pedwWatchCase", { path: "/" })), {} ); });