From f0fd86dd4863ac903d4e5611cc62c20966cb5a8e Mon Sep 17 00:00:00 2001 From: Vladimir Zhelvis Date: Wed, 1 Nov 2023 21:52:50 +0300 Subject: [PATCH] Pull request #695: AG-19767 Migrate Firefox add-on to event pages Merge in ADGUARD-FILTERS/tsurlfilter from feature/AG-19767 to master Squashed commit of the following: commit 8916997c73c396272c0fea0253a5de2b6a7b2265 Merge: b028a503 a1234dc3 Author: Vladimir Zhelvis Date: Wed Nov 1 13:13:27 2023 +0300 Merge branch 'master' into feature/AG-19767 commit b028a5034ee842d0764ac21ebda7c268e2d4c2af Author: Vladimir Zhelvis Date: Wed Nov 1 12:25:31 2023 +0300 update docs commit 7a21a1e23e24292965b9b8d944023cace81e646d Author: Vladimir Zhelvis Date: Wed Nov 1 12:06:16 2023 +0300 update tests commit f91948a24014d1f040d0e2a404516ab4818d72fc Author: Vladimir Zhelvis Date: Fri Oct 27 15:43:32 2023 +0300 delete deprecated todo commit 789e09aa67f4b7d13c013d22f94ffe5890ebc496 Author: Vladimir Zhelvis Date: Thu Oct 26 20:35:22 2023 +0300 update storage decorator commit d97218405148eb162f4a8b9c38aa81dab42dcf08 Author: Vladimir Zhelvis Date: Wed Oct 25 12:41:51 2023 +0300 update comment commit 5c8087e1ff0e524c6bc6e2a187ae82b5ad2c6a01 Author: Vladimir Zhelvis Date: Wed Oct 25 12:40:12 2023 +0300 update comments commit d801043775868c24b8548af4c9c759b58a041373 Author: Vladimir Zhelvis Date: Tue Oct 24 20:44:13 2023 +0300 update changelog commit 587e8f251e3d37096219cbacb8a41f7ada9bbfff Merge: ced0261a 6a209c6a Author: Vladimir Zhelvis Date: Tue Oct 24 20:32:39 2023 +0300 update persistent storages commit ced0261a7df0f3a0561a66630e47bb522c3648b2 Author: Vladimir Zhelvis Date: Tue Oct 24 20:29:50 2023 +0300 update persistent storages commit 9b681ca6a5a724a22910414ac290fa0dc4c3e596 Author: Vladimir Zhelvis Date: Tue Oct 17 12:56:07 2023 +0300 bump version commit fb6b6f0afc977690684195b12567263d0a7293cc Author: Vladimir Zhelvis Date: Tue Oct 17 12:55:50 2023 +0300 update changelog commit e22cda554ae52b555ea19753ebf419ed204b9f5c Author: Vladimir Zhelvis Date: Tue Oct 17 12:55:26 2023 +0300 update rollup config commit 0809fcecb31ffc5fce25218314580d81e78dc40b Author: Vladimir Zhelvis Date: Tue Oct 17 12:02:33 2023 +0300 fix PersistentMap util commit 9a35ea4ce8becb2e8e114d265653cc80d32401bd Merge: a3b3965c 1f1fe7e3 Author: Vladimir Zhelvis Date: Tue Oct 17 12:01:50 2023 +0300 Merge branch 'master' into feature/AG-19767 commit a3b3965cc4167410556e9393ba6692f82c15e0ac Author: Maxim Topciu Date: Fri Oct 13 13:26:00 2023 +0300 AG-19767 fix tests commit 6ba44109901ae73618f98e88fb3077078e1e981b Author: Vladimir Zhelvis Date: Fri Oct 13 10:54:21 2023 +0300 add persistent stores --- packages/tswebextension/.swcrc | 11 ++ packages/tswebextension/CHANGELOG.md | 6 + packages/tswebextension/jest.config.ts | 3 + packages/tswebextension/package.json | 4 +- packages/tswebextension/rollup.config.ts | 1 + packages/tswebextension/setupTests.ts | 2 + packages/tswebextension/src/cli/copyWar.ts | 7 +- .../tswebextension/src/lib/common/index.ts | 1 + .../storage/extension-storage-decorator.ts | 61 ++++++++ .../lib/common/storage/extension-storage.ts | 72 +++++++++ .../src/lib/common/storage/index.ts | 3 + .../storage/persistent-value-container.ts | 141 ++++++++++++++++++ .../src/lib/mv2/background/app.ts | 3 + .../src/lib/mv2/background/context.ts | 9 +- .../src/lib/mv2/background/index.ts | 1 + .../services/cookie-filtering/utils.ts | 2 +- .../src/lib/mv2/background/session-storage.ts | 41 +++++ .../src/lib/mv2/background/tabs/tabs-api.ts | 1 + .../extension-storage-decorator.test.ts | 57 +++++++ .../common/storage/extension-storage.test.ts | 51 +++++++ .../persistent-value-container.test.ts | 39 +++++ .../test/lib/mv2/background/app.test.ts | 6 +- .../lib/mv2/background/cosmetic-api.test.ts | 5 + .../lib/mv2/background/mocks/mock-context.ts | 11 ++ .../services/cookie-filtering/utils.test.ts | 2 + packages/tswebextension/tsconfig.base.json | 5 +- packages/tswebextension/yarn.lock | 25 +++- 27 files changed, 554 insertions(+), 16 deletions(-) create mode 100644 packages/tswebextension/.swcrc create mode 100644 packages/tswebextension/src/lib/common/storage/extension-storage-decorator.ts create mode 100644 packages/tswebextension/src/lib/common/storage/extension-storage.ts create mode 100644 packages/tswebextension/src/lib/common/storage/index.ts create mode 100644 packages/tswebextension/src/lib/common/storage/persistent-value-container.ts create mode 100644 packages/tswebextension/src/lib/mv2/background/session-storage.ts create mode 100644 packages/tswebextension/test/lib/common/storage/extension-storage-decorator.test.ts create mode 100644 packages/tswebextension/test/lib/common/storage/extension-storage.test.ts create mode 100644 packages/tswebextension/test/lib/common/storage/persistent-value-container.test.ts create mode 100644 packages/tswebextension/test/lib/mv2/background/mocks/mock-context.ts diff --git a/packages/tswebextension/.swcrc b/packages/tswebextension/.swcrc new file mode 100644 index 000000000..c18947b26 --- /dev/null +++ b/packages/tswebextension/.swcrc @@ -0,0 +1,11 @@ +{ + "jsc": { + "parser": { + "syntax": "typescript", + "decorators": true + }, + "transform": { + "decoratorVersion": "2022-03" + } + } +} \ No newline at end of file diff --git a/packages/tswebextension/CHANGELOG.md b/packages/tswebextension/CHANGELOG.md index ad94ee5c5..3db6cdef8 100644 --- a/packages/tswebextension/CHANGELOG.md +++ b/packages/tswebextension/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [Unreleased] + +### Added +- Added new `ExtensionStorage`, `PersistentValueContainer`, `createExtensionStorageDecorator` interfaces and for restoring data in event-driven background scripts [#2286](https://github.com/AdguardTeam/AdguardBrowserExtension/issues/2286). + + ## [0.4.2] - 2023-10-17 ### Fixed diff --git a/packages/tswebextension/jest.config.ts b/packages/tswebextension/jest.config.ts index 95a3d3200..81bd0ab7c 100644 --- a/packages/tswebextension/jest.config.ts +++ b/packages/tswebextension/jest.config.ts @@ -4,6 +4,9 @@ const config: Config = { transform: { '.+\\.(js|ts)': '@swc/jest', }, + transformIgnorePatterns: [ + 'node_modules/(?!(lodash-es)/)', + ], testEnvironment: 'jsdom', testRegex: '(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$', moduleFileExtensions: [ diff --git a/packages/tswebextension/package.json b/packages/tswebextension/package.json index fc64855cb..e07b750a7 100644 --- a/packages/tswebextension/package.json +++ b/packages/tswebextension/package.json @@ -88,6 +88,7 @@ "bowser": "2.11.0", "commander": "11.0.0", "fs-extra": "11.1.1", + "lodash-es": "^4.17.21", "lru_map": "0.4.1", "nanoid": "4.0.2", "text-encoding": "git+https://github.com/AdguardTeam/text-encoding.git#v0.7.2", @@ -106,8 +107,9 @@ "@types/chrome": "^0.0.237", "@types/fs-extra": "^11.0.1", "@types/jest": "29.5.2", + "@types/lodash-es": "^4.17.9", "@types/sinon-chrome": "2.2.11", - "@types/webextension-polyfill": "0.10.0", + "@types/webextension-polyfill": "^0.10.4", "@typescript-eslint/eslint-plugin": "5.59.11", "@typescript-eslint/parser": "5.59.11", "coveralls": "3.1.1", diff --git a/packages/tswebextension/rollup.config.ts b/packages/tswebextension/rollup.config.ts index 09c6c5bbe..342733878 100644 --- a/packages/tswebextension/rollup.config.ts +++ b/packages/tswebextension/rollup.config.ts @@ -115,6 +115,7 @@ const backgroundMv2Config = { 'deepmerge', 'nanoid', 'lru_map', + 'lodash-es', ], plugins: [ ...commonPlugins, diff --git a/packages/tswebextension/setupTests.ts b/packages/tswebextension/setupTests.ts index a2a5bbd7d..1c2982ee0 100644 --- a/packages/tswebextension/setupTests.ts +++ b/packages/tswebextension/setupTests.ts @@ -1,5 +1,7 @@ import browser from 'sinon-chrome'; +browser.runtime.getManifest.returns({ version: '2', manifest_version: 2 }); + jest.mock('webextension-polyfill', () => ({ ...browser, webRequest: { diff --git a/packages/tswebextension/src/cli/copyWar.ts b/packages/tswebextension/src/cli/copyWar.ts index 4f641ec23..1cfb9416f 100644 --- a/packages/tswebextension/src/cli/copyWar.ts +++ b/packages/tswebextension/src/cli/copyWar.ts @@ -1,6 +1,6 @@ +/* eslint-disable no-console */ import path from 'path'; import { copy } from 'fs-extra'; -import { logger } from '../lib/common/utils/logger'; const REDIRECTS_CONFIG_PATH = 'redirects.yml'; const REDIRECTS_RESOURCES_SRC_PATH = 'redirect-files'; @@ -8,6 +8,7 @@ const REDIRECTS_RESOURCES_DEST_PATH = 'redirects'; const src = path.resolve(require.resolve('@adguard/scriptlets'), '../..'); +// TODO: use logger from lib after import fix export const copyWar = async (dest: string): Promise => { dest = path.resolve(process.cwd(), dest); @@ -15,8 +16,8 @@ export const copyWar = async (dest: string): Promise => { await copy(path.resolve(src, REDIRECTS_CONFIG_PATH), path.resolve(dest, REDIRECTS_CONFIG_PATH)); await copy(path.resolve(src, REDIRECTS_RESOURCES_SRC_PATH), path.resolve(dest, REDIRECTS_RESOURCES_DEST_PATH)); - logger.info(`Web accessible resources was copied to ${dest}`); + console.info(`Web accessible resources was copied to ${dest}`); } catch (e) { - logger.error((e as Error).message); + console.error((e as Error).message); } }; diff --git a/packages/tswebextension/src/lib/common/index.ts b/packages/tswebextension/src/lib/common/index.ts index 00e4ea8cb..7fc97ada2 100644 --- a/packages/tswebextension/src/lib/common/index.ts +++ b/packages/tswebextension/src/lib/common/index.ts @@ -9,3 +9,4 @@ export * from './content-script/send-app-message'; export * from './request-type'; export * from './error'; export * from './constants'; +export * from './storage'; diff --git a/packages/tswebextension/src/lib/common/storage/extension-storage-decorator.ts b/packages/tswebextension/src/lib/common/storage/extension-storage-decorator.ts new file mode 100644 index 000000000..c7532c7e1 --- /dev/null +++ b/packages/tswebextension/src/lib/common/storage/extension-storage-decorator.ts @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { ExtensionStorage } from './extension-storage'; + +/** + * Creates accessor decorator for the specified storage. + * + * @param storage The extension storage API to use. + * @returns Accessor decorator for the specified storage. + * @see https://github.com/tc39/proposal-decorators + */ +export function createExtensionStorageDecorator>( + storage: ExtensionStorage, +) { + const fields = new Set(); + + /** + * Creates accessor decorator for the specified storage field. + * + * NOTE: You should not set the initial value to the accessor via assignment, + * because decorator overwrite accessors methods and don't use private property, created on initialization. + * Use Non-null assertion operator instead. + * @example `@storage('foo') accessor bar!: string`; + * @param field Storage field name. + * @throws Error if decorator is already registered for {@link field} + * or decorator is applied to class member different from auto accessor. + * @returns Decorator for access to specified storage {@link field}. + */ + return function createFieldDecorator(field: Field) { + // We prevent the use of multiple decorators on a single storage field, + // because manipulating data through the accessors of multiple modules can be confusing. + if (fields.has(field)) { + throw new Error(`Decorator for ${String(field)} field is already registered`); + } + + fields.add(field); + + return function fieldDecorator< + // The type on which the class element will be defined. + // For a static class element, this will be the type of the constructor. + // For a non-static class element, this will be the type of the instance. + This, + >( + _target: ClassAccessorDecoratorTarget, + context: ClassAccessorDecoratorContext, + ): ClassAccessorDecoratorResult | void { + if (context.kind !== 'accessor') { + throw new Error('Class member is not auto accessor'); + } + + // we do not set init descriptor, because data will be initialized asynchronously + return { + get(): Data[Field] { + return storage.get(field); + }, + set(value: Data[Field]): void { + return storage.set(field, value); + }, + }; + }; + }; +} diff --git a/packages/tswebextension/src/lib/common/storage/extension-storage.ts b/packages/tswebextension/src/lib/common/storage/extension-storage.ts new file mode 100644 index 000000000..9af3a6288 --- /dev/null +++ b/packages/tswebextension/src/lib/common/storage/extension-storage.ts @@ -0,0 +1,72 @@ +import type { Storage } from 'webextension-polyfill'; + +import { PersistentValueContainer } from './persistent-value-container'; + +/** + * API for storing persistent key-value data with debounced sync with the specified webextension storage key. + * Webextension storage synchronization described in the {@link PersistentValueContainer} class. + */ +export class ExtensionStorage< + Data extends Record, + Key extends string = string, +> { + /** + * API for storing persistent value with debounced sync with the specified webextension storage key. + */ + #container: PersistentValueContainer; + + /** + * Creates {@link ExtensionStorage} instance. + * @param key The key to use for storing the data. + * @param api Webextension storage API. + */ + constructor( + key: Key, + api: Storage.StorageArea, + ) { + this.#container = new PersistentValueContainer(key, api); + } + + /** + * Initializes the storage. + * @param data The initial data. + * @returns Promise that resolves when the storage is initialized. + * @throws Error, if storage already initialized. + */ + init(data: Data): Promise { + return this.#container.init(data); + } + + /** + * Gets the value by the specified key. + * @param key The key to use for storing the value. + * @throws Error, if storage not initialized. + * @returns Data stored by the specified key. + */ + get(key: T): Data[T] { + return this.#container.get()[key]; + } + + /** + * Sets the value by the specified key. + * @param key The key to use for storing the value. + * @param value New value. + * @throws Error, if storage not initialized. + */ + set(key: T, value: Data[T]): void { + const data = this.#container.get(); + data[key] = value; + this.#container.set(data); + } + + /** + * Deletes the value by the specified key. + * @param key The key to use for storing the value. + * @throws Error, if storage not initialized. + */ + delete(key: keyof Data): void { + const data = this.#container.get(); + delete data[key]; + this.#container.set(data); + } +} diff --git a/packages/tswebextension/src/lib/common/storage/index.ts b/packages/tswebextension/src/lib/common/storage/index.ts new file mode 100644 index 000000000..36217b744 --- /dev/null +++ b/packages/tswebextension/src/lib/common/storage/index.ts @@ -0,0 +1,3 @@ +export { PersistentValueContainer } from './persistent-value-container'; +export { ExtensionStorage } from './extension-storage'; +export { createExtensionStorageDecorator } from './extension-storage-decorator'; diff --git a/packages/tswebextension/src/lib/common/storage/persistent-value-container.ts b/packages/tswebextension/src/lib/common/storage/persistent-value-container.ts new file mode 100644 index 000000000..c0d8cc607 --- /dev/null +++ b/packages/tswebextension/src/lib/common/storage/persistent-value-container.ts @@ -0,0 +1,141 @@ +import { debounce } from 'lodash-es'; +import browser, { type Storage, type Manifest } from 'webextension-polyfill'; + +/** + * API to store a persistent value with debounced synchronization to the specified web extension storage key. + * + * After the container is created, we initialize it asynchronously to get the actual value from the storage. + * The Init method is guarded against multiple initializations to avoid unnecessary reads from the memory. + * Get/set methods are protected from uninitialized storage to ensure that actual data is used. + * + * We declare the sync get/set methods to update the cached value. This allows us to use containers in accessors. + * + * Set method updates the cached value and schedules the save operation to the storage via a debounce function to + * avoid unnecessary writes to the storage. + * + * This container saves the data to storage using the specified key to avoid collisions with other instances. + * It helps to avoid reading the data from the storage that is not related to the current instance. + */ +export class PersistentValueContainer { + // TODO: delete after the migration to event-driven background. + // We do not recalculate this value because the background type cannot change at runtime. + static #IS_BACKGROUND_PERSISTENT = PersistentValueContainer.#isBackgroundPersistent(); + + #api: Storage.StorageArea; + + #key: Key; + + #value!: Value; + + // TODO: make required after the migration to event-driven background. + #save?: () => void; + + #isInitialized = false; + + /** + * Creates {@link PersistentValueContainer} instance. + * @param key The key to use for storing the data. + * @param api Webextension storage API. + * @param debounceMs The debounce time in milliseconds to save the data to the storage. + * Optional. Default is 300ms. + */ + constructor( + key: Key, + api: Storage.StorageArea, + debounceMs = 300, + ) { + this.#key = key; + this.#api = api; + + /** + * TODO: remove this condition after the migration to event-driven background. + */ + if (!PersistentValueContainer.#IS_BACKGROUND_PERSISTENT) { + this.#save = debounce(() => { + this.#api.set({ [this.#key]: this.#value }); + }, debounceMs); + } + } + + /** + * Initializes the value. + * @param value The initial value. + * @returns Promise that resolves when the value is initialized. + * @throws Error, if storage already initialized. + */ + async init(value: Value): Promise { + if (this.#isInitialized) { + throw new Error('Storage already initialized'); + } + + if (PersistentValueContainer.#IS_BACKGROUND_PERSISTENT) { + this.#value = value; + } else { + const storageData = await this.#api.get({ + [this.#key]: value, + }); + + this.#value = storageData[this.#key]; + } + + this.#isInitialized = true; + } + + /** + * Gets the value. + * @returns The value stored by the specified key. + * @throws Error, if storage not initialized. + */ + get(): Value { + this.#checkIsInitialized(); + + return this.#value; + } + + /** + * Sets the value. + * @param value Value to be stored in the specified key. + * @throws Error, if storage not initialized. + */ + set(value: Value): void { + this.#checkIsInitialized(); + + this.#value = value; + + if (this.#save) { + this.#save(); + } + } + + /** + * Checks if the storage is initialized. + * @throws Error, if storage not initialized. + */ + #checkIsInitialized(): void { + if (!this.#isInitialized) { + throw new Error('Storage not initialized'); + } + } + + /** + * TODO: remove this method after the migration to event-driven background. + * Checks if the background script is persistent. + * @returns True if the background script is persistent. + */ + static #isBackgroundPersistent(): boolean { + const manifest = browser.runtime.getManifest(); + + if (manifest.manifest_version === 3) { + return false; + } + + if (!manifest.background) { + return true; + } + + const background = manifest.background as + (Manifest.WebExtensionManifestBackgroundC2Type | Manifest.WebExtensionManifestBackgroundC1Type); + + return background.persistent ?? true; + } +} diff --git a/packages/tswebextension/src/lib/mv2/background/app.ts b/packages/tswebextension/src/lib/mv2/background/app.ts index 5156b9625..c802c0e64 100644 --- a/packages/tswebextension/src/lib/mv2/background/app.ts +++ b/packages/tswebextension/src/lib/mv2/background/app.ts @@ -1,4 +1,5 @@ /* eslint-disable class-methods-use-this */ +import { sessionStorage } from './session-storage'; import { appContext } from './context'; import { WebRequestApi } from './web-request-api'; import { @@ -117,6 +118,8 @@ MessageHandlerMV2 * @throws Error if configuration is not valid. */ public async start(configuration: ConfigurationMV2): Promise { + await sessionStorage.init(); + configurationMV2Validator.parse(configuration); this.configuration = TsWebExtension.createConfigurationMV2Context(configuration); diff --git a/packages/tswebextension/src/lib/mv2/background/context.ts b/packages/tswebextension/src/lib/mv2/background/context.ts index 5f4b56fb8..add7f41f7 100644 --- a/packages/tswebextension/src/lib/mv2/background/context.ts +++ b/packages/tswebextension/src/lib/mv2/background/context.ts @@ -1,22 +1,25 @@ import type { ConfigurationMV2Context } from './configuration'; +import { sessionDecorator, SessionStorageKey } from './session-storage'; /** * Top level app context storage. * * This context is needed to share data between other modules without cyclic dependencies. * - * TODO (v.zhelvis) move app context to common and make it generic. + * TODO (v.zhelvis) delete this context after DI is implemented. */ export class AppContext { /** * Is app started. */ - isAppStarted = false; + @sessionDecorator(SessionStorageKey.IsAppStarted) + accessor isAppStarted!: boolean; /** * MV2 ConfigurationMV2 excludes heavyweight fields with rules. */ - configuration: ConfigurationMV2Context | undefined; + @sessionDecorator(SessionStorageKey.Configuration) + accessor configuration: ConfigurationMV2Context | undefined; } export const appContext = new AppContext(); diff --git a/packages/tswebextension/src/lib/mv2/background/index.ts b/packages/tswebextension/src/lib/mv2/background/index.ts index 0254da6e6..a4feedf89 100644 --- a/packages/tswebextension/src/lib/mv2/background/index.ts +++ b/packages/tswebextension/src/lib/mv2/background/index.ts @@ -8,6 +8,7 @@ export { export * from './api'; export * from './app'; +export * from './session-storage'; export * from './tabs'; export * from './request'; export * from '../../common'; diff --git a/packages/tswebextension/src/lib/mv2/background/services/cookie-filtering/utils.ts b/packages/tswebextension/src/lib/mv2/background/services/cookie-filtering/utils.ts index 9115a10ed..77e1b2440 100644 --- a/packages/tswebextension/src/lib/mv2/background/services/cookie-filtering/utils.ts +++ b/packages/tswebextension/src/lib/mv2/background/services/cookie-filtering/utils.ts @@ -1,7 +1,7 @@ import { WebRequest } from 'webextension-polyfill'; import ParsedCookie from './parsed-cookie'; import HttpHeadersItemType = WebRequest.HttpHeadersItemType; -import { logger } from '../..'; +import { logger } from '../../../../common/utils/logger'; /** * Cookie Utils. diff --git a/packages/tswebextension/src/lib/mv2/background/session-storage.ts b/packages/tswebextension/src/lib/mv2/background/session-storage.ts new file mode 100644 index 000000000..9b22e29e1 --- /dev/null +++ b/packages/tswebextension/src/lib/mv2/background/session-storage.ts @@ -0,0 +1,41 @@ +import browser from 'webextension-polyfill'; +import { ExtensionStorage, createExtensionStorageDecorator } from '../../common/storage'; +import type { ConfigurationMV2Context } from './configuration'; + +export const enum SessionStorageKey { + IsAppStarted = 'isAppStarted', + Configuration = 'configuration', +} + +export type SessionStorageSchema = { + [SessionStorageKey.IsAppStarted]: boolean, + [SessionStorageKey.Configuration]: ConfigurationMV2Context | undefined, +}; + +/** + * API for storing data described by {@link SessionStorageSchema} in the {@link browser.storage.session}. + */ +export class SessionStorage extends ExtensionStorage { + static readonly #DOMAIN = 'tswebextension'; + + static readonly #DEFAULT_DATA: SessionStorageSchema = { + isAppStarted: false, + configuration: undefined, + }; + + /** + * Creates {@link SessionStorage} instance. + */ + constructor() { + super(SessionStorage.#DOMAIN, browser.storage.session); + } + + /** @inheritdoc */ + override init(): Promise { + return super.init(SessionStorage.#DEFAULT_DATA); + } +} + +export const sessionStorage = new SessionStorage(); + +export const sessionDecorator = createExtensionStorageDecorator(sessionStorage); diff --git a/packages/tswebextension/src/lib/mv2/background/tabs/tabs-api.ts b/packages/tswebextension/src/lib/mv2/background/tabs/tabs-api.ts index 013e910fd..e38cf95df 100644 --- a/packages/tswebextension/src/lib/mv2/background/tabs/tabs-api.ts +++ b/packages/tswebextension/src/lib/mv2/background/tabs/tabs-api.ts @@ -17,6 +17,7 @@ export type TabFrameRequestContext = FrameRequestContext & { * Tabs API. Wrapper around browser.tabs API. */ export class TabsApi { + // TODO: Use a persistent map when the extended serialization is implemented. (AG-27098) public context = new Map(); public onCreate = new EventChannel(); diff --git a/packages/tswebextension/test/lib/common/storage/extension-storage-decorator.test.ts b/packages/tswebextension/test/lib/common/storage/extension-storage-decorator.test.ts new file mode 100644 index 000000000..995f64019 --- /dev/null +++ b/packages/tswebextension/test/lib/common/storage/extension-storage-decorator.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable jsdoc/require-jsdoc */ +import browser from 'webextension-polyfill'; +import { createExtensionStorageDecorator } from '@lib/common/storage/extension-storage-decorator'; +import { ExtensionStorage } from '@lib/common/storage/extension-storage'; + +describe('createExtensionStorageDecorator', () => { + const key = 'test-key'; + const data = { foo: 'bar', baz: 42 }; + let storage: ExtensionStorage; + + beforeAll(async () => { + storage = new ExtensionStorage(key, browser.storage.local); + await storage.init(data); + }); + + it('should create a decorator', () => { + const decorator = createExtensionStorageDecorator(storage)('foo'); + + expect(typeof decorator).toBe('function'); + }); + + it('should throw an error if decorator is already registered for the storage field', () => { + const decorator = createExtensionStorageDecorator(storage); + decorator('foo'); + expect(() => decorator('foo')).toThrow( + 'Decorator for foo field is already registered', + ); + }); + + it('should throw an error if decorator is applied to non-auto accessor', () => { + const fieldDecorator = createExtensionStorageDecorator(storage)('foo'); + + // Required for test runtime errors + // @ts-ignore + expect(() => fieldDecorator({}, { kind: 'method' })).toThrow( + 'Class member is not auto accessor', + ); + }); + + it('should get and set the value of the specified field', () => { + const decorator = createExtensionStorageDecorator(storage); + + class TestClass { + @decorator('foo') + accessor foo!: string + } + + const instance = new TestClass(); + + expect(instance.foo).toBe('bar'); + + instance.foo = 'new-value'; + + expect(instance.foo).toBe('new-value'); + expect(storage.get('foo')).toBe('new-value'); + }); +}); diff --git a/packages/tswebextension/test/lib/common/storage/extension-storage.test.ts b/packages/tswebextension/test/lib/common/storage/extension-storage.test.ts new file mode 100644 index 000000000..cbda49608 --- /dev/null +++ b/packages/tswebextension/test/lib/common/storage/extension-storage.test.ts @@ -0,0 +1,51 @@ +import browser from 'webextension-polyfill'; +import { ExtensionStorage } from '@lib/common/storage/extension-storage'; + +describe('ExtensionStorage', () => { + const key = 'test-key'; + const data = { foo: 'bar', baz: 42 }; + const api = browser.storage.local; + + it('should initialize the storage', async () => { + const storage = new ExtensionStorage(key, api); + await storage.init(data); + + expect(storage.get('foo')).toBe('bar'); + expect(storage.get('baz')).toBe(42); + }); + + it('should get the value by the specified key', async () => { + const storage = new ExtensionStorage(key, api); + await storage.init(data); + + expect(storage.get('foo')).toBe('bar'); + expect(storage.get('baz')).toBe(42); + }); + + it('should set the value by the specified key', async () => { + const storage = new ExtensionStorage(key, api); + await storage.init(data); + + storage.set('foo', 'new-bar'); + + expect(storage.get('foo')).toBe('new-bar'); + }); + + it('should delete the value by the specified key', async () => { + const storage = new ExtensionStorage(key, api); + await storage.init(data); + + storage.delete('foo'); + + expect(storage.get('foo')).toBeUndefined(); + expect(storage.get('baz')).toBe(42); + }); + + it('should throw an error if storage is not initialized', () => { + const storage = new ExtensionStorage(key, api); + + expect(() => storage.get('foo')).toThrow('Storage not initialized'); + expect(() => storage.set('foo', 'bar')).toThrow('Storage not initialized'); + expect(() => storage.delete('foo')).toThrow('Storage not initialized'); + }); +}); diff --git a/packages/tswebextension/test/lib/common/storage/persistent-value-container.test.ts b/packages/tswebextension/test/lib/common/storage/persistent-value-container.test.ts new file mode 100644 index 000000000..4fa3d7d44 --- /dev/null +++ b/packages/tswebextension/test/lib/common/storage/persistent-value-container.test.ts @@ -0,0 +1,39 @@ +import browser from 'webextension-polyfill'; +import { PersistentValueContainer } from '@lib/common/storage/persistent-value-container'; + +describe('PersistentValueContainer', () => { + const key = 'test-key'; + const value = 'test-value'; + const api = browser.storage.local; + + it('should initialize the value', async () => { + const container = new PersistentValueContainer(key, api); + await container.init(value); + + expect(container.get()).toBe(value); + }); + + it('should set the value', async () => { + const container = new PersistentValueContainer(key, api); + await container.init(value); + + const newValue = 'new-value'; + container.set(newValue); + + expect(container.get()).toBe(newValue); + }); + + it('should throw an error if storage is not initialized', () => { + const container = new PersistentValueContainer(key, browser.storage.local); + + expect(() => container.get()).toThrow('Storage not initialized'); + expect(() => container.set(value)).toThrow('Storage not initialized'); + }); + + it('should throw an error if storage is already initialized', async () => { + const container = new PersistentValueContainer(key, browser.storage.local); + await container.init(value); + + await expect(container.init(value)).rejects.toThrow('Storage already initialized'); + }); +}); diff --git a/packages/tswebextension/test/lib/mv2/background/app.test.ts b/packages/tswebextension/test/lib/mv2/background/app.test.ts index 3c2a361f7..6316784be 100644 --- a/packages/tswebextension/test/lib/mv2/background/app.test.ts +++ b/packages/tswebextension/test/lib/mv2/background/app.test.ts @@ -7,8 +7,12 @@ import { messagesApi } from '@lib/mv2/background/messages-api'; import type { ConfigurationMV2 } from '@lib/mv2/background/configuration'; import type { Message } from '@lib/common/message'; import { getConfigurationMv2Fixture } from './fixtures/configuration'; +import { MockAppContext } from './mocks/mock-context'; -jest.mock('@lib/mv2/background/context'); +jest.mock('@lib/mv2/background/session-storage'); +jest.mock('@lib/mv2/background/context', () => ({ + appContext: jest.fn(() => new MockAppContext()), +})); jest.mock('@lib/mv2/background/web-request-api'); jest.mock('@lib/mv2/background/engine-api'); jest.mock('@lib/mv2/background/tabs/tabs-api'); diff --git a/packages/tswebextension/test/lib/mv2/background/cosmetic-api.test.ts b/packages/tswebextension/test/lib/mv2/background/cosmetic-api.test.ts index edf5d64c8..81c98afb8 100644 --- a/packages/tswebextension/test/lib/mv2/background/cosmetic-api.test.ts +++ b/packages/tswebextension/test/lib/mv2/background/cosmetic-api.test.ts @@ -4,6 +4,11 @@ import { localScriptRulesService } from '@lib/mv2/background/services/local-scri import { CosmeticResult, CosmeticRule } from '@adguard/tsurlfilter'; import { USER_FILTER_ID } from '@lib/common/constants'; import { getLocalScriptRulesFixture } from './fixtures/local-script-rules'; +import { MockAppContext } from './mocks/mock-context'; + +jest.mock('@lib/mv2/background/context', () => ({ + appContext: jest.fn(() => new MockAppContext()), +})); /** * Creates cosmetic result for elemhide rules. diff --git a/packages/tswebextension/test/lib/mv2/background/mocks/mock-context.ts b/packages/tswebextension/test/lib/mv2/background/mocks/mock-context.ts new file mode 100644 index 000000000..bf11efb03 --- /dev/null +++ b/packages/tswebextension/test/lib/mv2/background/mocks/mock-context.ts @@ -0,0 +1,11 @@ +import type { ConfigurationMV2Context } from '@lib/mv2/background/configuration'; +import type { AppContext } from '@lib/mv2/background/context'; + +/** + * Mock for {@link AppContext}. + */ +export class MockAppContext implements AppContext { + isAppStarted: boolean = false; + + configuration: ConfigurationMV2Context | undefined = undefined; +} diff --git a/packages/tswebextension/test/lib/mv2/background/services/cookie-filtering/utils.test.ts b/packages/tswebextension/test/lib/mv2/background/services/cookie-filtering/utils.test.ts index 48324f262..dd246ece0 100644 --- a/packages/tswebextension/test/lib/mv2/background/services/cookie-filtering/utils.test.ts +++ b/packages/tswebextension/test/lib/mv2/background/services/cookie-filtering/utils.test.ts @@ -3,6 +3,8 @@ import ParsedCookie from '@lib/mv2/background/services/cookie-filtering/parsed-c const TEST_URL = 'https://test.com/url'; +jest.mock('@lib/common/utils/logger'); + describe('Cookie utils - Set-Cookie headers parsing', () => { it('checks parse simple', () => { let cookies: ParsedCookie[] = CookieUtils.parseSetCookieHeaders([], TEST_URL); diff --git a/packages/tswebextension/tsconfig.base.json b/packages/tswebextension/tsconfig.base.json index 8dd453dc4..7ea39d91d 100644 --- a/packages/tswebextension/tsconfig.base.json +++ b/packages/tswebextension/tsconfig.base.json @@ -1,12 +1,11 @@ { "compilerOptions": { "moduleResolution": "node", - "target": "es2015", - "module": "es2015", + "target": "es2022", + "module": "es2022", "lib": ["esnext", "dom"], "strict": true, "allowJs": true, - "experimentalDecorators": true, "resolveJsonModule": true, "esModuleInterop": true, // This will set allowSyntheticDefaultImports to true "typeRoots": ["./types", "node_modules/@types"], diff --git a/packages/tswebextension/yarn.lock b/packages/tswebextension/yarn.lock index ae4cc418d..e00b6f2d2 100644 --- a/packages/tswebextension/yarn.lock +++ b/packages/tswebextension/yarn.lock @@ -1031,6 +1031,18 @@ dependencies: "@types/node" "*" +"@types/lodash-es@^4.17.9": + version "4.17.9" + resolved "https://registry.yarnpkg.com/@types/lodash-es/-/lodash-es-4.17.9.tgz#49dbe5112e23c54f2b387d860b7d03028ce170c2" + integrity sha512-ZTcmhiI3NNU7dEvWLZJkzG6ao49zOIjEgIE0RgV7wbPxU0f2xT3VSAHw2gmst8swH6V0YkLRGp4qPlX/6I90MQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.199" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" + integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== + "@types/node@*": version "20.3.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" @@ -1081,10 +1093,10 @@ resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== -"@types/webextension-polyfill@0.10.0": - version "0.10.0" - resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#e87b5e2c101599779a584cdb043887ad73b37b0e" - integrity sha512-If4EcaHzYTqcbNMp/FdReVdRmLL/Te42ivnJII551bYjhX19bWem5m14FERCqdJA732OloGuxCRvLBvcMGsn4A== +"@types/webextension-polyfill@^0.10.4": + version "0.10.4" + resolved "https://registry.yarnpkg.com/@types/webextension-polyfill/-/webextension-polyfill-0.10.4.tgz#09feeb2d8f04ac0a28818ade8aeeb4ab9fbafebb" + integrity sha512-pvEIqAZEbJRzaqTaWq3xlUoMWa3+euZHHz+VZHCzHWW+jOf8qLOq9wXy38U+WiPG3108SJC/wNc1X6vPC5TcjQ== "@types/yargs-parser@*": version "21.0.0" @@ -3606,6 +3618,11 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"