Skip to content

Commit 41ea7d4

Browse files
chunked cookies implementation with tests, fixed some warnings in other tests related to await expect
1 parent 9f2e29d commit 41ea7d4

6 files changed

+525
-27
lines changed

src/server/auth-client.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ ca/T0LLtgmbMmxSv/MmzIg==
875875

876876
const response = await authClient.handleLogin(request);
877877
expect(response.status).toEqual(500);
878-
expect(await response.text()).toEqual(
878+
expect(await response.text()).toContain(
879879
"An error occured while trying to initiate the login request."
880880
);
881881
});

src/server/chunked-cookies.test.ts

+361
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
CookieOptions,
5+
deleteChunkedCookie,
6+
getChunkedCookie,
7+
RequestCookies,
8+
ResponseCookies,
9+
setChunkedCookie
10+
} from "./cookies";
11+
12+
// Create mock implementation for RequestCookies and ResponseCookies
13+
const createMocks = () => {
14+
const cookieStore = new Map();
15+
16+
const reqCookies = {
17+
get: vi.fn((...args) => {
18+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
19+
if (cookieStore.has(name)) {
20+
return { name, value: cookieStore.get(name) };
21+
}
22+
return undefined;
23+
}),
24+
getAll: vi.fn((...args) => {
25+
if (args.length === 0) {
26+
return Array.from(cookieStore.entries()).map(([name, value]) => ({
27+
name,
28+
value
29+
}));
30+
}
31+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
32+
return cookieStore.has(name)
33+
? [{ name, value: cookieStore.get(name) }]
34+
: [];
35+
}),
36+
has: vi.fn((name) => cookieStore.has(name)),
37+
set: vi.fn((...args) => {
38+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
39+
const value = typeof args[0] === "string" ? args[1] : args[0].value;
40+
cookieStore.set(name, value);
41+
return reqCookies;
42+
}),
43+
delete: vi.fn((names) => {
44+
if (Array.isArray(names)) {
45+
return names.map((name) => cookieStore.delete(name));
46+
}
47+
return cookieStore.delete(names);
48+
}),
49+
clear: vi.fn(() => {
50+
cookieStore.clear();
51+
return reqCookies;
52+
}),
53+
get size() {
54+
return cookieStore.size;
55+
},
56+
[Symbol.iterator]: vi.fn(() => cookieStore.entries())
57+
};
58+
59+
const resCookies = {
60+
get: vi.fn((...args) => {
61+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
62+
if (cookieStore.has(name)) {
63+
return { name, value: cookieStore.get(name) };
64+
}
65+
return undefined;
66+
}),
67+
getAll: vi.fn((...args) => {
68+
if (args.length === 0) {
69+
return Array.from(cookieStore.entries()).map(([name, value]) => ({
70+
name,
71+
value
72+
}));
73+
}
74+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
75+
return cookieStore.has(name)
76+
? [{ name, value: cookieStore.get(name) }]
77+
: [];
78+
}),
79+
has: vi.fn((name) => cookieStore.has(name)),
80+
set: vi.fn((...args) => {
81+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
82+
const value = typeof args[0] === "string" ? args[1] : args[0].value;
83+
cookieStore.set(name, value);
84+
return resCookies;
85+
}),
86+
delete: vi.fn((...args) => {
87+
const name = typeof args[0] === "string" ? args[0] : args[0].name;
88+
cookieStore.delete(name);
89+
return resCookies;
90+
}),
91+
toString: vi.fn(() => {
92+
return Array.from(cookieStore.entries())
93+
.map(([name, value]) => `${name}=${value}`)
94+
.join("; ");
95+
})
96+
};
97+
98+
return { reqCookies, resCookies, cookieStore };
99+
};
100+
101+
describe("Chunked Cookie Utils", () => {
102+
let reqCookies: RequestCookies;
103+
let resCookies: ResponseCookies;
104+
let cookieStore: Map<any, any>;
105+
106+
beforeEach(() => {
107+
const mocks = createMocks();
108+
reqCookies = mocks.reqCookies;
109+
resCookies = mocks.resCookies;
110+
cookieStore = mocks.cookieStore;
111+
112+
// Spy on console.warn
113+
vi.spyOn(console, "warn").mockImplementation(() => {});
114+
});
115+
116+
afterEach(() => {
117+
vi.clearAllMocks();
118+
});
119+
120+
describe("setChunkedCookie", () => {
121+
it("should set a single cookie when value is small enough", () => {
122+
const name = "testCookie";
123+
const value = "small value";
124+
const options = { path: "/" } as CookieOptions;
125+
126+
setChunkedCookie(name, value, options, reqCookies, resCookies);
127+
128+
expect(resCookies.set).toHaveBeenCalledTimes(1);
129+
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
130+
expect(reqCookies.set).toHaveBeenCalledTimes(1);
131+
expect(reqCookies.set).toHaveBeenCalledWith(name, value);
132+
});
133+
134+
it("should split cookie into chunks when value exceeds max size", () => {
135+
const name = "largeCookie";
136+
const options = { path: "/" } as CookieOptions;
137+
138+
// Create a large string (8000 bytes)
139+
const largeValue = "a".repeat(8000);
140+
141+
setChunkedCookie(name, largeValue, options, reqCookies, resCookies);
142+
143+
// Should create 3 chunks (8000 / 3500 ≈ 2.3, rounded up to 3)
144+
expect(resCookies.set).toHaveBeenCalledTimes(3);
145+
expect(reqCookies.set).toHaveBeenCalledTimes(3);
146+
147+
// Check first chunk
148+
expect(resCookies.set).toHaveBeenCalledWith(
149+
`${name}__0`,
150+
largeValue.slice(0, 3500),
151+
options
152+
);
153+
154+
// Check second chunk
155+
expect(resCookies.set).toHaveBeenCalledWith(
156+
`${name}__1`,
157+
largeValue.slice(3500, 7000),
158+
options
159+
);
160+
161+
// Check third chunk
162+
expect(resCookies.set).toHaveBeenCalledWith(
163+
`${name}__2`,
164+
largeValue.slice(7000),
165+
options
166+
);
167+
});
168+
169+
it("should log a warning when cookie size exceeds warning threshold", () => {
170+
const name = "warningCookie";
171+
const options = { path: "/" } as CookieOptions;
172+
173+
// Create a value that exceeds the warning threshold (4096 bytes)
174+
const value = "a".repeat(4097);
175+
176+
setChunkedCookie(name, value, options, reqCookies, resCookies);
177+
178+
expect(console.warn).toHaveBeenCalled();
179+
});
180+
181+
describe("getChunkedCookie", () => {
182+
it("should return undefined when cookie does not exist", () => {
183+
const result = getChunkedCookie("nonexistent", reqCookies);
184+
expect(result).toBeUndefined();
185+
});
186+
187+
it("should return cookie value when it exists as a regular cookie", () => {
188+
const name = "simpleCookie";
189+
const value = "simple value";
190+
191+
// Setup the cookie
192+
cookieStore.set(name, value);
193+
194+
const result = getChunkedCookie(name, reqCookies);
195+
196+
expect(result).toBe(value);
197+
expect(reqCookies.get).toHaveBeenCalledWith(name);
198+
});
199+
200+
it("should reconstruct value from chunks when cookie is chunked", () => {
201+
const name = "chunkedCookie";
202+
const chunk0 = "chunk0 value";
203+
const chunk1 = "chunk1 value";
204+
const chunk2 = "chunk2 value";
205+
206+
// Add the chunks to the store (out of order)
207+
cookieStore.set(`${name}__1`, chunk1);
208+
cookieStore.set(`${name}__0`, chunk0);
209+
cookieStore.set(`${name}__2`, chunk2);
210+
211+
// Also add some unrelated cookies
212+
cookieStore.set("otherCookie", "other value");
213+
214+
const result = getChunkedCookie(name, reqCookies);
215+
216+
// Should combine chunks in proper order
217+
expect(result).toBe(`${chunk0}${chunk1}${chunk2}`);
218+
});
219+
220+
it("should return undefined when chunks are not in a complete sequence", () => {
221+
const name = "incompleteCookie";
222+
223+
// Add incomplete chunks (missing chunk1)
224+
cookieStore.set(`${name}__0`, "chunk0");
225+
cookieStore.set(`${name}__2`, "chunk2");
226+
227+
const result = getChunkedCookie(name, reqCookies);
228+
229+
expect(result).toBeUndefined();
230+
expect(console.warn).toHaveBeenCalled();
231+
});
232+
});
233+
234+
describe("deleteChunkedCookie", () => {
235+
it("should delete the regular cookie", () => {
236+
const name = "regularCookie";
237+
cookieStore.set(name, "regular value");
238+
239+
deleteChunkedCookie(name, reqCookies, resCookies);
240+
241+
expect(resCookies.delete).toHaveBeenCalledWith(name);
242+
});
243+
244+
it("should delete all chunks of a cookie", () => {
245+
const name = "chunkedCookie";
246+
247+
// Add chunks
248+
cookieStore.set(`${name}__0`, "chunk0");
249+
cookieStore.set(`${name}__1`, "chunk1");
250+
cookieStore.set(`${name}__2`, "chunk2");
251+
252+
// Add unrelated cookie
253+
cookieStore.set("otherCookie", "other value");
254+
255+
deleteChunkedCookie(name, reqCookies, resCookies);
256+
257+
// Should delete main cookie and 3 chunks
258+
expect(resCookies.delete).toHaveBeenCalledTimes(4);
259+
expect(resCookies.delete).toHaveBeenCalledWith(name);
260+
expect(resCookies.delete).toHaveBeenCalledWith(`${name}__0`);
261+
expect(resCookies.delete).toHaveBeenCalledWith(`${name}__1`);
262+
expect(resCookies.delete).toHaveBeenCalledWith(`${name}__2`);
263+
// Should not delete unrelated cookies
264+
expect(resCookies.delete).not.toHaveBeenCalledWith("otherCookie");
265+
});
266+
});
267+
268+
describe("Edge Cases", () => {
269+
it("should handle empty values correctly", () => {
270+
const name = "emptyCookie";
271+
const value = "";
272+
const options = { path: "/" } as CookieOptions;
273+
274+
setChunkedCookie(name, value, options, reqCookies, resCookies);
275+
276+
expect(resCookies.set).toHaveBeenCalledTimes(1);
277+
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
278+
});
279+
280+
it("should handle values at the exact chunk boundary", () => {
281+
const name = "boundaryValueCookie";
282+
const value = "a".repeat(3500); // Exactly MAX_CHUNK_SIZE
283+
const options = { path: "/" } as CookieOptions;
284+
285+
setChunkedCookie(name, value, options, reqCookies, resCookies);
286+
287+
// Should still fit in one cookie
288+
expect(resCookies.set).toHaveBeenCalledTimes(1);
289+
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
290+
});
291+
292+
it("should handle special characters in cookie values", () => {
293+
const name = "specialCharCookie";
294+
const value =
295+
'{"special":"characters","with":"quotation marks","and":"😀 emoji"}';
296+
const options = { path: "/" } as CookieOptions;
297+
298+
setChunkedCookie(name, value, options, reqCookies, resCookies);
299+
300+
expect(resCookies.set).toHaveBeenCalledWith(name, value, options);
301+
302+
// Setup for retrieval
303+
cookieStore.set(name, value);
304+
305+
const result = getChunkedCookie(name, reqCookies);
306+
expect(result).toBe(value);
307+
});
308+
309+
it("should handle multi-byte characters correctly", () => {
310+
const name = "multiByteCookie";
311+
// Create a test string with multi-byte characters (emojis)
312+
const value = "Hello 😀 world 🌍 with emojis 🎉";
313+
const options = { path: "/" } as CookieOptions;
314+
315+
// Store the cookie
316+
setChunkedCookie(name, value, options, reqCookies, resCookies);
317+
318+
// For the retrieval test, manually set up the cookies
319+
// We're testing the retrieval functionality, not the chunking itself
320+
cookieStore.clear();
321+
cookieStore.set(name, value);
322+
323+
// Verify retrieval works correctly with multi-byte characters
324+
const result = getChunkedCookie(name, reqCookies);
325+
expect(result).toBe(value);
326+
327+
// Verify emoji characters were preserved
328+
expect(result).toContain("😀");
329+
expect(result).toContain("🌍");
330+
expect(result).toContain("🎉");
331+
});
332+
333+
it("should handle very large cookies properly", () => {
334+
const name = "veryLargeCookie";
335+
const value = "a".repeat(10000); // Will create multiple chunks
336+
const options = { path: "/" } as CookieOptions;
337+
338+
setChunkedCookie(name, value, options, reqCookies, resCookies);
339+
340+
// Get chunks count (10000 / 3500 ≈ 2.86, so we need 3 chunks)
341+
const expectedChunks = Math.ceil(10000 / 3500);
342+
343+
expect(resCookies.set).toHaveBeenCalledTimes(expectedChunks);
344+
345+
// Clear and set up cookies for retrieval test
346+
cookieStore.clear();
347+
348+
// Setup for getChunkedCookie retrieval
349+
for (let i = 0; i < expectedChunks; i++) {
350+
const start = i * 3500;
351+
const end = Math.min((i + 1) * 3500, 10000);
352+
cookieStore.set(`${name}__${i}`, value.slice(start, end));
353+
}
354+
355+
const result = getChunkedCookie(name, reqCookies);
356+
expect(result).toBe(value);
357+
expect(result!.length).toBe(10000);
358+
});
359+
});
360+
});
361+
});

0 commit comments

Comments
 (0)