UNPKG

chrome-devtools-frontend

Version:
435 lines (363 loc) • 17.1 kB
// Copyright 2022 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as Common from '../../core/common/common.js'; import * as SDK from '../../core/sdk/sdk.js'; import * as Protocol from '../../generated/protocol.js'; import {createTarget} from '../../testing/EnvironmentHelpers.js'; import {describeWithMockConnection} from '../../testing/MockConnection.js'; import { getInitializedResourceTreeModel, getMainFrame, MAIN_FRAME_ID, navigate, } from '../../testing/ResourceTreeHelpers.js'; import * as Resources from './application.js'; class SharedStorageListener { #model: Resources.SharedStorageModel.SharedStorageModel; #storagesWatched: Resources.SharedStorageModel.SharedStorageForOrigin[]; #accessEvents: Protocol.Storage.SharedStorageAccessedEvent[]; #changeEvents: Map<Resources.SharedStorageModel.SharedStorageForOrigin, Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent[]>; constructor(model: Resources.SharedStorageModel.SharedStorageModel) { this.#model = model; this.#storagesWatched = []; this.#accessEvents = []; this.#changeEvents = new Map< Resources.SharedStorageModel.SharedStorageForOrigin, Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent[]>(); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED, this.#sharedStorageAdded, this); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_REMOVED, this.#sharedStorageRemoved, this); this.#model.addEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ACCESS, this.#sharedStorageAccess, this); } dispose(): void { this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED, this.#sharedStorageAdded, this); this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_REMOVED, this.#sharedStorageRemoved, this); this.#model.removeEventListener( Resources.SharedStorageModel.Events.SHARED_STORAGE_ACCESS, this.#sharedStorageAccess, this); for (const storage of this.#storagesWatched) { storage.removeEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); } } get accessEvents(): Protocol.Storage.SharedStorageAccessedEvent[] { return this.#accessEvents; } changeEventsForStorage(storage: Resources.SharedStorageModel.SharedStorageForOrigin): Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent[]|null { return this.#changeEvents.get(storage) || null; } changeEventsEmpty(): boolean { return this.#changeEvents.size === 0; } #sharedStorageAdded(event: Common.EventTarget.EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin>): void { const storage = (event.data); this.#storagesWatched.push(storage); storage.addEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); } #sharedStorageRemoved( event: Common.EventTarget.EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin>): void { const storage = (event.data); storage.removeEventListener( Resources.SharedStorageModel.SharedStorageForOrigin.Events.SHARED_STORAGE_CHANGED, this.#sharedStorageChanged.bind(this, storage), this); const index = this.#storagesWatched.indexOf(storage); if (index === -1) { return; } this.#storagesWatched = this.#storagesWatched.splice(index, 1); } #sharedStorageAccess(event: Common.EventTarget.EventTargetEvent<Protocol.Storage.SharedStorageAccessedEvent>): void { this.#accessEvents.push(event.data); } #sharedStorageChanged( storage: Resources.SharedStorageModel.SharedStorageForOrigin, event: Common.EventTarget .EventTargetEvent<Resources.SharedStorageModel.SharedStorageForOrigin.SharedStorageChangedEvent>): void { if (!this.#changeEvents.has(storage)) { this.#changeEvents.set(storage, []); } this.#changeEvents.get(storage)?.push(event.data); } async waitForStoragesAdded(expectedCount: number): Promise<void> { while (this.#storagesWatched.length < expectedCount) { await this.#model.once(Resources.SharedStorageModel.Events.SHARED_STORAGE_ADDED); } } } describeWithMockConnection('SharedStorageModel', () => { let sharedStorageModel: Resources.SharedStorageModel.SharedStorageModel; let target: SDK.Target.Target; let listener: SharedStorageListener; const TEST_ORIGIN_A = 'http://a.test'; const TEST_SITE_A = TEST_ORIGIN_A; const TEST_ORIGIN_B = 'http://b.test'; const TEST_SITE_B = TEST_ORIGIN_B; const TEST_ORIGIN_C = 'http://c.test'; const TEST_SITE_C = TEST_ORIGIN_C; const METADATA = { creationTime: 100 as Protocol.Network.TimeSinceEpoch, length: 3, remainingBudget: 2.5, bytesUsed: 30, } as unknown as Protocol.Storage.SharedStorageMetadata; const ENTRIES = [ { key: 'key1', value: 'a', } as unknown as Protocol.Storage.SharedStorageEntry, { key: 'key2', value: 'b', } as unknown as Protocol.Storage.SharedStorageEntry, { key: 'key3', value: 'c', } as unknown as Protocol.Storage.SharedStorageEntry, ]; const EVENTS = [ { accessTime: 0, method: Protocol.Storage.SharedStorageAccessMethod.Append, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_A, ownerSite: TEST_SITE_A, params: {key: 'key0', value: 'value0'} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.Window, }, { accessTime: 10, method: Protocol.Storage.SharedStorageAccessMethod.Get, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_A, ownerSite: TEST_SITE_A, params: {key: 'key0'} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet, }, { accessTime: 15, method: Protocol.Storage.SharedStorageAccessMethod.Length, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_B, ownerSite: TEST_SITE_B, params: {} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet, }, { accessTime: 20, method: Protocol.Storage.SharedStorageAccessMethod.Clear, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_B, ownerSite: TEST_SITE_B, params: {} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.Window, }, { accessTime: 100, method: Protocol.Storage.SharedStorageAccessMethod.Set, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_C, ownerSite: TEST_SITE_C, params: {key: 'key0', value: 'value1', ignoreIfPresent: true} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet, }, { accessTime: 150, method: Protocol.Storage.SharedStorageAccessMethod.RemainingBudget, mainFrameId: MAIN_FRAME_ID, ownerOrigin: TEST_ORIGIN_C, ownerSite: TEST_SITE_C, params: {} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet, }, ]; beforeEach(async () => { target = createTarget(); await getInitializedResourceTreeModel(target); sharedStorageModel = target.model(Resources.SharedStorageModel.SharedStorageModel) as Resources.SharedStorageModel.SharedStorageModel; listener = new SharedStorageListener(sharedStorageModel); }); it('invokes storageAgent via SharedStorageForOrigin', async () => { const getMetadataSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageMetadata').resolves({ metadata: METADATA, getError: () => undefined, }); const getEntriesSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_getSharedStorageEntries').resolves({ entries: ENTRIES, getError: () => undefined, }); const setEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageEntry').resolves({ getError: () => undefined, }); const deleteEntrySpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_deleteSharedStorageEntry').resolves({ getError: () => undefined, }); const clearSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_clearSharedStorageEntries').resolves({ getError: () => undefined, }); const sharedStorage = new Resources.SharedStorageModel.SharedStorageForOrigin(sharedStorageModel, TEST_ORIGIN_A); assert.strictEqual(sharedStorage.securityOrigin, TEST_ORIGIN_A); const metadata = await sharedStorage.getMetadata(); sinon.assert.calledOnceWithExactly(getMetadataSpy, {ownerOrigin: TEST_ORIGIN_A}); assert.deepEqual(METADATA, metadata); const entries = await sharedStorage.getEntries(); sinon.assert.calledOnceWithExactly(getEntriesSpy, {ownerOrigin: TEST_ORIGIN_A}); assert.deepEqual(ENTRIES, entries); await sharedStorage.setEntry('new-key1', 'new-value1', true); sinon.assert.calledOnceWithExactly( setEntrySpy, {ownerOrigin: TEST_ORIGIN_A, key: 'new-key1', value: 'new-value1', ignoreIfPresent: true}); await sharedStorage.deleteEntry('new-key1'); sinon.assert.calledOnceWithExactly(deleteEntrySpy, {ownerOrigin: TEST_ORIGIN_A, key: 'new-key1'}); await sharedStorage.clear(); sinon.assert.calledOnceWithExactly(clearSpy, {ownerOrigin: TEST_ORIGIN_A}); }); it('adds/removes SharedStorageForOrigin on SecurityOrigin events', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); assert.isEmpty(sharedStorageModel.storages()); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); const addedPromise = listener.waitForStoragesAdded(1); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginAdded, TEST_ORIGIN_A); await addedPromise; assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginRemoved, TEST_ORIGIN_A); assert.isEmpty(sharedStorageModel.storages()); }); it('does not add SharedStorageForOrigin if origin invalid', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); assert.isEmpty(sharedStorageModel.storages()); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); manager.dispatchEventToListeners(SDK.SecurityOriginManager.Events.SecurityOriginAdded, 'invalid'); assert.isEmpty(sharedStorageModel.storages()); }); it('does not add SharedStorageForOrigin if origin already added', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); assert.isEmpty(sharedStorageModel.storages()); const addedPromise = listener.waitForStoragesAdded(1); navigate(getMainFrame(target), {url: TEST_ORIGIN_A}); await addedPromise; assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); assert.strictEqual(1, sharedStorageModel.numStoragesForTesting()); navigate(getMainFrame(target), {url: TEST_ORIGIN_A}); assert.strictEqual(1, sharedStorageModel.numStoragesForTesting()); }); it('adds/removes SecurityOrigins when model is enabled/disabled', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); const originSet = new Set([TEST_ORIGIN_A, TEST_ORIGIN_B, TEST_ORIGIN_C]); manager.updateSecurityOrigins(originSet); assert.lengthOf(manager.securityOrigins(), 3); const addedPromise = listener.waitForStoragesAdded(3); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); await addedPromise; assert.strictEqual(3, sharedStorageModel.numStoragesForTesting()); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_A)); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_B)); assert.exists(sharedStorageModel.storageForOrigin(TEST_ORIGIN_C)); sharedStorageModel.disable(); assert.isEmpty(sharedStorageModel.storages()); }); it('dispatches SharedStorageAccess events to listeners', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } assert.deepEqual(EVENTS, listener.accessEvents); }); it('dispatches SharedStorageChanged events to listeners', async () => { const setTrackingSpy = sinon.stub(sharedStorageModel.storageAgent, 'invoke_setSharedStorageTracking').resolves({ getError: () => undefined, }); const manager = target.model(SDK.SecurityOriginManager.SecurityOriginManager); assert.exists(manager); await sharedStorageModel.enable(); sinon.assert.calledOnceWithExactly(setTrackingSpy, {enable: true}); // For change events whose origins aren't yet in the model, the origin is added // to the model, with the `SharedStorageAdded` event being subsequently dispatched // instead of the `SharedStorageChanged` event. const addedPromise = listener.waitForStoragesAdded(3); for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } await addedPromise; assert.strictEqual(4, sharedStorageModel.numStoragesForTesting()); assert.deepEqual(EVENTS, listener.accessEvents); assert.isTrue(listener.changeEventsEmpty()); // All events will be dispatched as `SharedStorageAccess` events, but only change // events for existing origins will be forwarded as `SharedStorageChanged` events. for (const event of EVENTS) { sharedStorageModel.sharedStorageAccessed(event); } assert.deepEqual(EVENTS.concat(EVENTS), listener.accessEvents); const storageA = sharedStorageModel.storageForOrigin(TEST_ORIGIN_A); assert.exists(storageA); assert.deepEqual(listener.changeEventsForStorage(storageA), [ { accessTime: 0, method: Protocol.Storage.SharedStorageAccessMethod.Append, mainFrameId: MAIN_FRAME_ID, ownerSite: TEST_SITE_A, params: {key: 'key0', value: 'value0'} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.Window, }, ]); const storageB = sharedStorageModel.storageForOrigin(TEST_ORIGIN_B); assert.exists(storageB); assert.deepEqual(listener.changeEventsForStorage(storageB), [ { accessTime: 20, method: Protocol.Storage.SharedStorageAccessMethod.Clear, mainFrameId: MAIN_FRAME_ID, ownerSite: TEST_SITE_B, params: {} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.Window, }, ]); const storageC = sharedStorageModel.storageForOrigin(TEST_ORIGIN_C); assert.exists(storageC); assert.deepEqual(listener.changeEventsForStorage(storageC), [ { accessTime: 100, method: Protocol.Storage.SharedStorageAccessMethod.Set, mainFrameId: MAIN_FRAME_ID, ownerSite: TEST_SITE_C, params: {key: 'key0', value: 'value1', ignoreIfPresent: true} as Protocol.Storage.SharedStorageAccessParams, scope: Protocol.Storage.SharedStorageAccessScope.SharedStorageWorklet, }, ]); }); });