| // Copyright 2022 The Chromium Authors |
| // 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, |
| }, |
| ]); |
| }); |
| }); |