Skip to content

Commit 7d6c604

Browse files
committed
Update tests
1 parent a1941cd commit 7d6c604

File tree

7 files changed

+120
-189
lines changed

7 files changed

+120
-189
lines changed

packages/cache-handlers/src/read.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,24 @@ export async function readFromCache(
6262
const now = Date.now();
6363
if (!isNaN(expiresAt) && now >= expiresAt) {
6464
let swrSeconds: number | undefined;
65-
const cc = cachedResponse.headers.get("cache-control");
66-
if (cc) {
67-
const directives = parseCacheControl(cc);
68-
if (typeof directives["stale-while-revalidate"] === "number") {
69-
swrSeconds = directives["stale-while-revalidate"] as number;
65+
66+
// Check cdn-cache-control first (takes precedence)
67+
const cdnCc = cachedResponse.headers.get("cdn-cache-control");
68+
if (cdnCc) {
69+
const cdnDirectives = parseCacheControl(cdnCc);
70+
if (typeof cdnDirectives["stale-while-revalidate"] === "number") {
71+
swrSeconds = cdnDirectives["stale-while-revalidate"] as number;
72+
}
73+
}
74+
75+
// Fallback to regular cache-control if not found in cdn-cache-control
76+
if (swrSeconds === undefined) {
77+
const cc = cachedResponse.headers.get("cache-control");
78+
if (cc) {
79+
const directives = parseCacheControl(cc);
80+
if (typeof directives["stale-while-revalidate"] === "number") {
81+
swrSeconds = directives["stale-while-revalidate"] as number;
82+
}
7083
}
7184
}
7285
if (swrSeconds && now < expiresAt + swrSeconds * 1000) {

packages/cache-handlers/test/deno/conditional.test.ts

Lines changed: 9 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import { assert, assertEquals, assertExists } from "jsr:@std/assert";
22
import {
3-
compareETags,
43
create304Response,
54
generateETag,
6-
getDefaultConditionalConfig,
7-
parseETag,
8-
parseHttpDate,
9-
parseIfNoneMatch,
105
validateConditionalRequest,
116
} from "../../src/conditional.ts";
127
import { createCacheHandler } from "../../src/handlers.ts";
@@ -24,73 +19,6 @@ Deno.test("Conditional Requests - ETag generation", async () => {
2419
assert(etag.endsWith('"'));
2520
});
2621

27-
Deno.test("Conditional Requests - ETag parsing", () => {
28-
// Strong ETag
29-
const strongETag = parseETag('"abc123"');
30-
assertEquals(strongETag.value, "abc123");
31-
assertEquals(strongETag.weak, false);
32-
33-
// Weak ETag
34-
const weakETag = parseETag('W/"abc123"');
35-
assertEquals(weakETag.value, "abc123");
36-
assert(weakETag.weak);
37-
38-
// Empty ETag
39-
const emptyETag = parseETag("");
40-
assertEquals(emptyETag.value, "");
41-
assertEquals(emptyETag.weak, false);
42-
});
43-
44-
Deno.test("Conditional Requests - ETag comparison", () => {
45-
const etag1 = '"abc123"';
46-
const etag2 = '"abc123"';
47-
const etag3 = '"def456"';
48-
const weakETag = 'W/"abc123"';
49-
50-
// Strong comparison - exact match
51-
assertEquals(compareETags(etag1, etag2), true);
52-
assertEquals(compareETags(etag1, etag3), false);
53-
54-
// Strong comparison - weak ETag should not match
55-
assertEquals(compareETags(etag1, weakETag, false), false);
56-
57-
// Weak comparison - should match even with weak ETag
58-
assertEquals(compareETags(etag1, weakETag, true), true);
59-
});
60-
61-
Deno.test("Conditional Requests - If-None-Match parsing", () => {
62-
// Single ETag
63-
const single = parseIfNoneMatch('"abc123"');
64-
assert(Array.isArray(single));
65-
assertEquals((single as string[]).length, 1);
66-
assertEquals((single as string[])[0], '"abc123"');
67-
68-
// Multiple ETags
69-
const multiple = parseIfNoneMatch('"abc123", "def456", W/"ghi789"');
70-
assert(Array.isArray(multiple));
71-
assertEquals((multiple as string[]).length, 3);
72-
73-
// Wildcard
74-
const wildcard = parseIfNoneMatch("*");
75-
assertEquals(wildcard, "*");
76-
77-
// Empty
78-
const empty = parseIfNoneMatch("");
79-
assert(Array.isArray(empty));
80-
assertEquals((empty as string[]).length, 0);
81-
});
82-
83-
Deno.test("Conditional Requests - HTTP date parsing", () => {
84-
const validDate = parseHttpDate("Wed, 21 Oct 2015 07:28:00 GMT");
85-
assertExists(validDate);
86-
assert(validDate instanceof Date);
87-
88-
const invalidDate = parseHttpDate("invalid date");
89-
assertEquals(invalidDate, null);
90-
91-
const emptyDate = parseHttpDate("");
92-
assertEquals(emptyDate, null);
93-
});
9422

9523
Deno.test("Conditional Requests - validateConditionalRequest with ETag", () => {
9624
const request = new Request("https://example.com/test", {
@@ -176,9 +104,8 @@ Deno.test("Conditional Requests - 304 response creation", () => {
176104
assertEquals(response304.headers.get("x-custom"), null);
177105
});
178106

179-
// New unified handler integration tests
180107

181-
Deno.test("Conditional Requests - unified handler If-None-Match", async () => {
108+
Deno.test("Conditional Requests - unified handler returns 304 for matching ETag", async () => {
182109
await caches.delete("conditional-test");
183110
const cacheName = "conditional-test";
184111
const cache = await caches.open(cacheName);
@@ -202,12 +129,13 @@ Deno.test("Conditional Requests - unified handler If-None-Match", async () => {
202129
{ handler: () => Promise.resolve(new Response("fresh")) },
203130
);
204131
assertExists(result);
205-
assertEquals([200, 304].includes(result.status), true);
206-
await result.clone().text();
132+
assertEquals(result.status, 304);
133+
assertEquals(result.body, null);
134+
assertEquals(result.headers.get("etag"), '"test-etag-123"');
207135
await caches.delete("conditional-test");
208136
});
209137

210-
Deno.test("Conditional Requests - unified handler If-Modified-Since", async () => {
138+
Deno.test("Conditional Requests - unified handler returns 304 for matching Last-Modified", async () => {
211139
await caches.delete("conditional-test-date");
212140
const cacheName = "conditional-test-date";
213141
const cache = await caches.open(cacheName);
@@ -232,8 +160,10 @@ Deno.test("Conditional Requests - unified handler If-Modified-Since", async () =
232160
{ handler: () => Promise.resolve(new Response("fresh")) },
233161
);
234162
assertExists(result);
235-
assertEquals([200, 304].includes(result.status), true);
236-
await result.clone().text();
163+
assertEquals(result.status, 304);
164+
assertEquals(result.body, null);
165+
assertEquals(result.headers.get("last-modified"), lastModified);
166+
assertEquals(result.headers.get("content-type"), "application/json");
237167
await caches.delete(cacheName);
238168
});
239169

@@ -264,13 +194,6 @@ Deno.test("Conditional Requests - unified handler ETag generation", async () =>
264194
await caches.delete(cacheName);
265195
});
266196

267-
Deno.test("Conditional Requests - Default configuration", () => {
268-
const config = getDefaultConditionalConfig();
269-
270-
assert(config.etag);
271-
assert(config.lastModified);
272-
assert(config.weakValidation);
273-
});
274197

275198
Deno.test("Conditional Requests - disabled returns full response", async () => {
276199
await caches.delete("conditional-disabled-test");

packages/cache-handlers/test/deno/handlers.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { assert, assertEquals, assertExists } from "jsr:@std/assert";
22
import { createCacheHandler } from "../../src/handlers.ts";
33
import { assertSpyCalls, spy } from "jsr:@std/testing/mock";
44

5-
// Unified handler tests replacing legacy read/write/middleware handlers
65

76
Deno.test("cache miss invokes handler and caches response", async () => {
87
await caches.delete("test-miss");

packages/cache-handlers/test/deno/security.test.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
parseResponseHeaders,
88
} from "../../src/utils.ts";
99
import { invalidateByTag } from "../../src/invalidation.ts";
10+
import { createCacheHandler } from "../../src/handlers.ts";
1011

1112
Deno.test("Security - Header injection via cache tags", () => {
1213
// Test that cache tags with newlines/CRLF are properly handled
@@ -59,7 +60,7 @@ Deno.test("Security - Cache pollution via tag injection", async () => {
5960
// Simple test: just verify that malicious tags don't cause prototype pollution
6061
const maliciousResponse = new Response("data", {
6162
headers: {
62-
"cache-control": "max-age=3600, public",
63+
"cache-control": "s-maxage=3600, public",
6364
"cache-tag": "user:123, __proto__:polluted, admin:true",
6465
},
6566
});
@@ -103,7 +104,7 @@ Deno.test("Security - Cache key collision attack", () => {
103104
query: [],
104105
});
105106

106-
// Document the actual behaviour - collision vulnerability is now fixed with :: separators
107+
// Keys use :: separators to prevent collisions
107108
assertEquals(key1, "https://example.com/api/users|admin:true");
108109
assertEquals(key2, "https://example.com/api/users::h=admin:true");
109110

@@ -131,7 +132,7 @@ Deno.test("Security - Metadata size bomb", async () => {
131132
const hugeTags = Array.from({ length: 101 }, (_, i) => `tag:${i}`);
132133
const response = new Response("test data", {
133134
headers: {
134-
"cache-control": "max-age=3600, public",
135+
"cache-control": "s-maxage=3600, public",
135136
"cache-tag": hugeTags.join(", "),
136137
},
137138
});
@@ -158,3 +159,88 @@ Deno.test("Security - Metadata size bomb", async () => {
158159
}
159160
await caches.delete("test");
160161
});
162+
163+
Deno.test("Security - Integration: cache tags work correctly for invalidation", async () => {
164+
await caches.delete("security-integration");
165+
const handle = createCacheHandler({
166+
cacheName: "security-integration",
167+
});
168+
169+
// Test that cache-tag headers enable proper cache invalidation
170+
const response = new Response("tagged content", {
171+
headers: {
172+
"cache-control": "s-maxage=3600, public",
173+
"cache-tag": "user:123, sensitive:data",
174+
"content-type": "application/json",
175+
},
176+
});
177+
Object.defineProperty(response, "url", {
178+
value: "https://example.com/api/secure",
179+
writable: false,
180+
});
181+
182+
const request = new Request("https://example.com/api/secure");
183+
const result = await handle(request, {
184+
handler: () => Promise.resolve(response),
185+
});
186+
187+
// Verify response is cached and tags are preserved
188+
assertEquals(await result.text(), "tagged content");
189+
assertEquals(result.headers.get("content-type"), "application/json");
190+
assertEquals(result.headers.get("cache-tag"), "user:123, sensitive:data");
191+
192+
// Verify content is cached
193+
const cache = await caches.open("security-integration");
194+
const cached = await cache.match(request);
195+
assertEquals(cached !== undefined, true);
196+
if (cached) {
197+
await cached.text(); // Clean up the resource
198+
}
199+
200+
// Verify invalidation by tag works
201+
const deletedCount = await invalidateByTag("user:123", { cacheName: "security-integration" });
202+
assertEquals(deletedCount, 1);
203+
204+
// Verify content is gone after invalidation
205+
const afterInvalidation = await cache.match(request);
206+
assertEquals(afterInvalidation, undefined);
207+
208+
await caches.delete("security-integration");
209+
});
210+
211+
Deno.test("Security - Integration: CDN cache control prevents cache poisoning", async () => {
212+
await caches.delete("security-cdn");
213+
const handle = createCacheHandler({
214+
cacheName: "security-cdn",
215+
});
216+
217+
// Simulate response with cdn-cache-control that should override regular cache-control
218+
const response = new Response("sensitive data", {
219+
headers: {
220+
"cache-control": "s-maxage=86400, public", // Long cache
221+
"cdn-cache-control": "private, no-cache", // Should prevent caching
222+
"content-type": "application/json",
223+
},
224+
});
225+
Object.defineProperty(response, "url", {
226+
value: "https://example.com/api/sensitive",
227+
writable: false,
228+
});
229+
230+
const request = new Request("https://example.com/api/sensitive");
231+
const result = await handle(request, {
232+
handler: () => Promise.resolve(response),
233+
});
234+
235+
// Verify response is served
236+
assertEquals(await result.text(), "sensitive data");
237+
// Verify cdn-cache-control header is filtered from response
238+
assertEquals(result.headers.get("cdn-cache-control"), null);
239+
240+
// Verify content was not cached due to cdn-cache-control: private
241+
const cache = await caches.open("security-cdn");
242+
const cached = await cache.match(request);
243+
assertEquals(cached, undefined);
244+
245+
await caches.delete("security-cdn");
246+
});

packages/cache-handlers/test/node/conditional.test.ts

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,11 @@
11
import { describe, expect, test, vi } from "vitest";
22
import {
3-
compareETags,
43
create304Response,
5-
generateETag,
6-
parseETag,
74
validateConditionalRequest,
85
} from "../../src/conditional.ts";
96
import { createCacheHandler } from "../../src/handlers.ts";
107

118
describe("Conditional Requests - Node.js with undici", () => {
12-
describe("ETag utilities", () => {
13-
test("generates valid ETags", async () => {
14-
const response = new Response("test content", {
15-
headers: { "content-type": "text/plain" },
16-
});
17-
18-
const etag = await generateETag(response);
19-
20-
expect(etag).toBeTruthy();
21-
expect(typeof etag).toBe("string");
22-
expect(etag.startsWith('"')).toBe(true);
23-
expect(etag.endsWith('"')).toBe(true);
24-
});
25-
26-
test("parses ETags correctly", () => {
27-
// Strong ETag
28-
const strongETag = parseETag('"abc123"');
29-
expect(strongETag.value).toBe("abc123");
30-
expect(strongETag.weak).toBe(false);
31-
32-
// Weak ETag
33-
const weakETag = parseETag('W/"abc123"');
34-
expect(weakETag.value).toBe("abc123");
35-
expect(weakETag.weak).toBe(true);
36-
});
37-
38-
test("compares ETags correctly", () => {
39-
const etag1 = '"abc123"';
40-
const etag2 = '"abc123"';
41-
const etag3 = '"def456"';
42-
const weakETag = 'W/"abc123"';
43-
44-
// Strong comparison
45-
expect(compareETags(etag1, etag2)).toBe(true);
46-
expect(compareETags(etag1, etag3)).toBe(false);
47-
expect(compareETags(etag1, weakETag, false)).toBe(false);
48-
49-
// Weak comparison
50-
expect(compareETags(etag1, weakETag, true)).toBe(true);
51-
});
52-
});
53-
549
describe("Conditional validation", () => {
5510
test("validates ETag conditional requests", () => {
5611
const request = new Request("https://example.com/test", {

packages/cache-handlers/test/node/ttl-normalization.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe("TTL Normalization", () => {
6868
expect(handler).toHaveBeenCalledTimes(1);
6969
expect(await response.text()).toBe("cdn-max-age data");
7070
expect(response.headers.has("cache-tag")).toBe(true);
71-
expect(response.headers.has("cdn-cache-control")).toBe(false); // Should be removed
71+
expect(response.headers.has("cdn-cache-control")).toBe(false); // CDN headers filtered from response
7272

7373
// Second request should be cached
7474
const cachedResponse = await handle(request, { handler });
@@ -111,10 +111,10 @@ describe("TTL Normalization", () => {
111111

112112
const response = await handle(request, { handler });
113113

114-
// Check that used directives are removed but others remain
114+
// Check that CDN directives are filtered but browser directives remain
115115
const cacheControl = response.headers.get("cache-control");
116-
expect(cacheControl).not.toContain("s-maxage"); // Should be removed
117-
expect(cacheControl).not.toContain("stale-while-revalidate"); // Should be removed
116+
expect(cacheControl).not.toContain("s-maxage"); // CDN directive filtered out
117+
expect(cacheControl).not.toContain("stale-while-revalidate"); // CDN directive filtered out
118118
expect(cacheControl).toContain("max-age=7200"); // Should remain (for browsers)
119119
expect(cacheControl).toContain("public"); // Should remain
120120
});

0 commit comments

Comments
 (0)