diff --git a/src/browser/CdpCaptureStorage.test.ts b/src/browser/CdpCaptureStorage.test.ts index 870bed8c1b..d6699150cd 100644 --- a/src/browser/CdpCaptureStorage.test.ts +++ b/src/browser/CdpCaptureStorage.test.ts @@ -16,14 +16,15 @@ const NO_CACHE_COMMAND = Symbol("unsupported"); const captureWithCacheResult = async ( cacheResult: unknown, + storageEntries: unknown = [], ): Promise>["value"]> => { const connection = { - send(method: string): Promise { + send(method: string, params: Record): Promise { switch (method) { case "Storage.getUsageAndQuota": return Promise.resolve({ usage: 1, quota: 10 }); case "DOMStorage.getDOMStorageItems": - return Promise.resolve({ entries: [] }); + return Promise.resolve({ entries: storageEntries }); case "IndexedDB.requestDatabaseNames": return Promise.resolve({ databaseNames: [] }); case "Network.getCookies": @@ -40,6 +41,7 @@ const captureWithCacheResult = async ( ) : Promise.resolve(cacheResult); case "CacheStorage.requestEntries": + expect(params.cacheId).toEqual(expect.any(String)); return Promise.resolve({ cacheDataEntries: [], returnCount: 0 }); default: return Promise.reject(new Error(`Unexpected command: ${method}`)); @@ -362,3 +364,57 @@ it("keeps an opaque cookie partition unknown without dropping its value", async }); expect(storageComparison(plain, known).status).toBe("changed"); }); + +it("retains known storage keys and cache names when content or locator metadata is malformed", async () => { + const storage = await captureWithCacheResult( + { caches: [{ cacheName: "known-cache" }] }, + [ + ["known-key", 42], + ["valid-peer", "recorded"], + [42, "unknown identity"], + ], + ); + expect(storage.local_storage_keys).toEqual(["known-key", "valid-peer"]); + expect(storage.session_storage_keys).toEqual(["known-key", "valid-peer"]); + expect(storage.cache_names).toEqual(["known-cache"]); + expect(storage.fingerprints_complete).toBe(false); + const local = storage.content_fingerprints.filter( + ({ scope }) => scope === "local_storage", + ); + expect(local).toHaveLength(2); + expect(local).toContainEqual( + expect.objectContaining({ complete: false, value_sha256: null }), + ); + expect(local).toContainEqual( + expect.objectContaining({ + complete: true, + value_sha256: expect.any(String), + }), + ); +}); + +it("retains cookie identity when its content is malformed without inventing a value fingerprint", async () => { + const known = await captureCookies([cookieBase]); + const unavailable = await captureCookies([ + { ...cookieBase, value: 42 }, + { ...cookieBase, name: 42 }, + ]); + const knownCookie = known.content_fingerprints.find( + ({ scope }) => scope === "cookie", + ); + if (knownCookie === undefined) throw new Error("Missing known cookie"); + expect(unavailable.content_fingerprints).toEqual([ + { + scope: "cookie", + identity_sha256: knownCookie.identity_sha256, + value_sha256: null, + complete: false, + }, + ]); + expect(unavailable.fingerprints_complete).toBe(false); + expect(storageComparison(known, unavailable)).toMatchObject({ + status: "unknown", + total_changes: 0, + changes: [], + }); +}); diff --git a/src/browser/CdpCaptureStorage.ts b/src/browser/CdpCaptureStorage.ts index 9c3eae8c50..24c90e33b7 100644 --- a/src/browser/CdpCaptureStorage.ts +++ b/src/browser/CdpCaptureStorage.ts @@ -83,7 +83,8 @@ export const captureStorage = async ( // Like IndexedDB names, a missing, non-array or partly unusable cache // list is an incomplete enumeration rather than an empty one. Array.isArray(cacheField) && - caches.length === cacheField.length; + caches.length === cacheField.length && + caches.every(({ id }) => id !== undefined); const keysExclusion = !context.input.include_storage_keys ? undefined : local.exclusion === "provider_unavailable" || @@ -127,7 +128,7 @@ const capturedCaches = ( values.flatMap((cache) => { const name = cdpStringValue(cache.cacheName); const id = cdpStringValue(cache.cacheId); - return name === undefined || id === undefined ? [] : [{ name, id }]; + return name === undefined ? [] : [{ name, id }]; }); const storageItems = async ( @@ -154,9 +155,14 @@ const storageItems = async ( if (!Array.isArray(entry)) return []; const key = cdpStringValue(entry[0]); const value = cdpStringValue(entry[1]); - return key === undefined || value === undefined ? [] : [{ key, value }]; + return key === undefined ? [] : [{ key, value }]; }); - return { items, complete: items.length === entries.length }; + return { + items, + complete: + items.length === entries.length && + items.every(({ value }) => value !== undefined), + }; }; const stringArray = (value: unknown): readonly string[] => diff --git a/src/browser/CdpCaptureStorageFingerprints.ts b/src/browser/CdpCaptureStorageFingerprints.ts index 3d2e33103d..b01228f395 100644 --- a/src/browser/CdpCaptureStorageFingerprints.ts +++ b/src/browser/CdpCaptureStorageFingerprints.ts @@ -20,14 +20,14 @@ type StorageFingerprint = export interface CapturedStorageItems { readonly items: readonly { readonly key: string; - readonly value: string; + readonly value: string | undefined; }[]; readonly complete: boolean; } export interface CapturedCache { readonly name: string; - readonly id: string; + readonly id: string | undefined; } interface StorageFingerprintContext { @@ -106,7 +106,7 @@ const addStorageItems = ( scope, identity: item.key, value: item.value, - complete: true, + complete: item.value !== undefined, }); }; @@ -139,12 +139,7 @@ const addCookies = async ( const value = cdpStringValue(cookie.value); const domain = cdpStringValue(cookie.domain); const path = cdpStringValue(cookie.path); - if ( - name === undefined || - value === undefined || - domain === undefined || - path === undefined - ) { + if (name === undefined || domain === undefined || path === undefined) { run.state.complete = false; continue; } @@ -163,7 +158,7 @@ const addCookies = async ( ...(partitionOpaque ? { partition_opaque: true } : {}), }, value, - complete: !partitionOpaque, + complete: value !== undefined && !partitionOpaque, }); } }; @@ -357,6 +352,10 @@ const addCaches = async ( caches: readonly CapturedCache[], ): Promise => { for (const cache of caches) { + if (cache.id === undefined) { + run.state.complete = false; + continue; + } let skipCount = 0; let returnCount: number | undefined; while (returnCount === undefined || skipCount < returnCount) {