-
Notifications
You must be signed in to change notification settings - Fork 7
fix(sw): scope every fetch-handler caches.match() to its owned cache (#514) #612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+92
−8
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
efb4ad2
fix(sw): scope every fetch-handler caches.match() to its owned cache …
qnbs 1bc0275
docs: sync README test-count metrics for the new SW cache-scoping reg…
qnbs 26cc61e
test(sw): tighten cache-scoping regression assertions per review
qnbs 1db048d
docs: sync README test-count metrics for the tightened SW cache-scopi…
qnbs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| // @vitest-environment node | ||
| import { readFileSync } from 'node:fs'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| // QNBS-v3: regression guard for #514 — CacheStorage is origin-scoped, not path-scoped, so a bare | ||
| // caches.match(request) on a shared origin like qnbs.github.io searches every cache on the origin, | ||
| // not just this app's own. sw.js is a classic service worker (uses `self`, not importable), so we | ||
| // assert its source contract instead of executing it, mirroring swLocaleStrategy.test.ts's pattern. | ||
| const swSource = readFileSync( | ||
| fileURLToPath(new URL('../../public/sw.js', import.meta.url)), | ||
| 'utf8', | ||
| ); | ||
|
|
||
| /** Extract the body of the `self.addEventListener('fetch', ...)` handler. */ | ||
| function fetchHandlerBlock(src: string): string { | ||
| const start = src.indexOf("self.addEventListener('fetch'"); | ||
| expect(start).toBeGreaterThan(-1); | ||
| const end = src.indexOf("self.addEventListener('message'", start); | ||
| expect(end).toBeGreaterThan(start); | ||
| return src.slice(start, end); | ||
| } | ||
|
|
||
| /** Extract the body of the `offlineFallback` helper, called from every fetch-handler catch path. */ | ||
| function offlineFallbackBlock(src: string): string { | ||
| const start = src.indexOf('async function offlineFallback'); | ||
| expect(start).toBeGreaterThan(-1); | ||
| const end = src.indexOf('\n}', start); | ||
| expect(end).toBeGreaterThan(start); | ||
| return src.slice(start, end); | ||
| } | ||
|
|
||
| /** Every top-level `caches.match(...)` call found in a source block (not `cache.match(...)` on an already-opened, already-scoped handle). Strips `//` comments first so prose mentioning `caches.match()` can't masquerade as a real call site, and normalizes the `${BASE}` interpolation to a plain placeholder so expected-value strings in this file never need to embed a real template-literal placeholder themselves. */ | ||
| function cachesDotMatchCalls(block: string): string[] { | ||
| const codeOnly = block | ||
| .split('\n') | ||
| .map((line) => line.replace(/\/\/.*$/, '')) | ||
| .join('\n'); | ||
| const calls: string[] = []; | ||
| const re = /\bcaches\.match\([^;]*?\)/g; | ||
| let m: RegExpExecArray | null = re.exec(codeOnly); | ||
| while (m !== null) { | ||
| calls.push(m[0].replace(/\$\{BASE\}/, '<BASE>')); | ||
| m = re.exec(codeOnly); | ||
| } | ||
| return calls; | ||
| } | ||
|
|
||
| describe('service worker — caches.match() is always scoped to an owned cache (#514)', () => { | ||
| it('the fetch handler contains exactly the 3 known caches.match() call sites', () => { | ||
| // QNBS-v3: exact count, not a lower bound — a lower bound would let a call site silently disappear (e.g. an accidental merge/refactor) without this regression test failing. | ||
| const calls = cachesDotMatchCalls(fetchHandlerBlock(swSource)); | ||
| expect(calls.length).toBe(3); | ||
| }); | ||
|
|
||
| it('the JS/CSS Cache-First lookup reads from CACHE_STATIC, where the network path writes it', () => { | ||
| const start = swSource.indexOf('JS / CSS bundles'); | ||
| expect(start).toBeGreaterThan(-1); | ||
| const end = swSource.indexOf('Locale JSON', start); | ||
| expect(end).toBeGreaterThan(start); | ||
| const calls = cachesDotMatchCalls(swSource.slice(start, end)); | ||
| expect(calls).toEqual(['caches.match(request, { cacheName: CACHE_STATIC })']); | ||
| }); | ||
|
|
||
| it("the navigation fallback reads the navigated URL from CACHE_DYNAMIC and the SPA shell from CACHE_STATIC — not each other's cache", () => { | ||
| const start = swSource.indexOf('Navigation — Network First'); | ||
| expect(start).toBeGreaterThan(-1); | ||
| const end = swSource.indexOf('Everything else', start); | ||
| expect(end).toBeGreaterThan(start); | ||
| const calls = cachesDotMatchCalls(swSource.slice(start, end)); | ||
| expect(calls).toEqual([ | ||
| 'caches.match(request, { cacheName: CACHE_DYNAMIC })', | ||
| 'caches.match(`<BASE>index.html`, { cacheName: CACHE_STATIC })', | ||
| ]); | ||
| }); | ||
|
|
||
| it('offlineFallback (reachable from every fetch-handler catch path) reads offline.html from CACHE_STATIC, where it is precached', () => { | ||
| const calls = cachesDotMatchCalls(offlineFallbackBlock(swSource)); | ||
| expect(calls).toEqual(['caches.match(`<BASE>offline.html`, { cacheName: CACHE_STATIC })']); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.