mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
test(01-02): add red PWA cache isolation contracts
- exercise signed-in service-worker install at CacheStorage boundary - require data-free offline navigation and immutable public assets - require current and legacy session cache cleanup
This commit is contained in:
1 parent
668f85a58d
commit
3a813b51bd
3 files changed
+113
-1
No files matched your search
@@ -45,6 +45,8 @@ class FakeCache {
|
||||
putDelay: Promise<void> | undefined;
|
||||
onPut: (() => void) | undefined;
|
||||
keysError: Error | undefined;
|
||||
addAllResponse:
|
||||
((request: Request) => Promise<Response> | Response) | undefined;
|
||||
|
||||
private keyOf(request: RequestInfo | URL): string {
|
||||
if (typeof request === "string") return new URL(request, ORIGIN).href;
|
||||
@@ -65,7 +67,13 @@ class FakeCache {
|
||||
|
||||
async addAll(urls: string[]): Promise<void> {
|
||||
for (const url of urls) {
|
||||
this.map.set(this.keyOf(url), new Response(`precached:${url}`));
|
||||
const request = new Request(new URL(url, ORIGIN), {
|
||||
credentials: "same-origin",
|
||||
});
|
||||
const response = this.addAllResponse
|
||||
? await this.addAllResponse(request)
|
||||
: new Response(`precached:${url}`);
|
||||
this.map.set(this.keyOf(request), response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +207,16 @@ async function dispatchActivate(harness: SwHarness): Promise<void> {
|
||||
await settled;
|
||||
}
|
||||
|
||||
async function dispatchInstall(harness: SwHarness): Promise<void> {
|
||||
let settled: Promise<unknown> = Promise.resolve();
|
||||
harness.listeners.get("install")!({
|
||||
waitUntil: (p: Promise<unknown>) => {
|
||||
settled = p;
|
||||
},
|
||||
});
|
||||
await settled;
|
||||
}
|
||||
|
||||
function dispatchNavigationFetch(
|
||||
harness: SwHarness,
|
||||
path: string,
|
||||
@@ -474,6 +492,43 @@ describe("sw.js — activate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sw.js — authenticated install cache isolation", () => {
|
||||
it("never requests or stores signed-in root HTML while retaining public immutable assets", async () => {
|
||||
const harness = bootServiceWorker();
|
||||
const statics = await harness.cacheStorage.open(CURRENT_STATIC_CACHE);
|
||||
const requested: Request[] = [];
|
||||
const healthSentinel = "PRIVATE_DASHBOARD_WEIGHT_81_7_KG";
|
||||
statics.addAllResponse = async (request) => {
|
||||
requested.push(request);
|
||||
return new Response(
|
||||
new URL(request.url).pathname === "/"
|
||||
? `<html><body>${healthSentinel}</body></html>`
|
||||
: `public-asset:${new URL(request.url).pathname}`,
|
||||
);
|
||||
};
|
||||
|
||||
await dispatchInstall(harness);
|
||||
|
||||
expect
|
||||
.soft(requested.map((request) => new URL(request.url).pathname))
|
||||
.not.toContain("/");
|
||||
expect
|
||||
.soft(requested.every((request) => request.credentials === "same-origin"))
|
||||
.toBe(true);
|
||||
const cachedBodies = await Promise.all(
|
||||
[...statics.map.values()].map((response) => response.clone().text()),
|
||||
);
|
||||
expect.soft(cachedBodies.join("\n")).not.toContain(healthSentinel);
|
||||
expect(await statics.match(`${ORIGIN}/logo-192.png`)).toBeDefined();
|
||||
|
||||
const offline = await dispatchNavigationFetch(harness, "/");
|
||||
const offlineBody = await offline.text();
|
||||
expect.soft(offline.status).toBe(503);
|
||||
expect.soft(offlineBody).toContain("Offline");
|
||||
expect(offlineBody).not.toContain(healthSentinel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sw.js — networkFirst offline fallback", () => {
|
||||
it("never serves a shell cached under a previous CACHE_VERSION", async () => {
|
||||
const harness = bootServiceWorker();
|
||||
|
||||
@@ -79,6 +79,17 @@ describe("public/sw.js offline fallback locale neutrality (F-7, 2026-05-16)", ()
|
||||
});
|
||||
});
|
||||
|
||||
describe("public/sw.js authenticated install isolation (SEC-02)", () => {
|
||||
it("precaches public immutable assets but never the authenticated root route", () => {
|
||||
const sw = readSw();
|
||||
const precache = /const PRECACHE_URLS = \[([^\]]+)\]/.exec(sw)?.[1];
|
||||
|
||||
expect(precache).toBeDefined();
|
||||
expect.soft(precache).not.toMatch(/["']\/["']/);
|
||||
expect(precache).toMatch(/\/(?:logo|icons)\/?[^"']*\.(?:png|svg)/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("public/manifest.json PWA identity hygiene (F-7, 2026-05-16)", () => {
|
||||
it("declares an explicit scope, id, and display_override", () => {
|
||||
const raw = readFileSync(MANIFEST_PATH, "utf-8");
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
clearOfflineCachesForSessionEnd,
|
||||
clearPersistedQueryCache,
|
||||
isPersistableKey,
|
||||
restorePersistedQueryCache,
|
||||
@@ -86,6 +87,51 @@ describe("persister degrades gracefully without IndexedDB", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("session-end CacheStorage cleanup", () => {
|
||||
const originalCaches = (globalThis as { caches?: CacheStorage }).caches;
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCaches === undefined) {
|
||||
delete (globalThis as { caches?: CacheStorage }).caches;
|
||||
} else {
|
||||
(globalThis as { caches?: CacheStorage }).caches = originalCaches;
|
||||
}
|
||||
});
|
||||
|
||||
it("deletes current and legacy HealthLog static/page/data caches only", async () => {
|
||||
const cacheNames = [
|
||||
"healthlog-static-v1.34.0",
|
||||
"healthlog-static-v1.15.20",
|
||||
"healthlog-pages-v1.34.0",
|
||||
"healthlog-pages-v1.15.20",
|
||||
"healthlog-data-v1.34.0",
|
||||
"healthlog-data-v1.15.20",
|
||||
"unrelated-app-static-v7",
|
||||
"workbox-precache-healthlog",
|
||||
];
|
||||
const deleted: string[] = [];
|
||||
(globalThis as { caches?: CacheStorage }).caches = {
|
||||
keys: async () => cacheNames,
|
||||
delete: async (name: string) => {
|
||||
deleted.push(name);
|
||||
return true;
|
||||
},
|
||||
} as CacheStorage;
|
||||
|
||||
await clearOfflineCachesForSessionEnd();
|
||||
|
||||
expect
|
||||
.soft(deleted.sort())
|
||||
.toEqual(
|
||||
cacheNames
|
||||
.filter((name) => /^healthlog-(?:static|pages|data)-/.test(name))
|
||||
.sort(),
|
||||
);
|
||||
expect(deleted).not.toContain("unrelated-app-static-v7");
|
||||
expect(deleted).not.toContain("workbox-precache-healthlog");
|
||||
});
|
||||
});
|
||||
|
||||
describe("persister round-trips through a fake IndexedDB", () => {
|
||||
const original = (globalThis as { indexedDB?: unknown }).indexedDB;
|
||||
|
||||
|
||||
Reference in new issue
Block a user