mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
refactor(storage-test-utils): cut a new library for storage test tools (#15259)
* refactor(platform): generate a storage-test-utils library * refactor(storage-test-utils): move FakeStorageService out of common
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -91,6 +91,7 @@ libs/common/spec @bitwarden/team-platform-dev
|
|||||||
libs/common/src/state-migrations @bitwarden/team-platform-dev
|
libs/common/src/state-migrations @bitwarden/team-platform-dev
|
||||||
libs/platform @bitwarden/team-platform-dev
|
libs/platform @bitwarden/team-platform-dev
|
||||||
libs/storage-core @bitwarden/team-platform-dev
|
libs/storage-core @bitwarden/team-platform-dev
|
||||||
|
libs/storage-test-utils @bitwarden/team-platform-dev
|
||||||
# Web utils used across app and connectors
|
# Web utils used across app and connectors
|
||||||
apps/web/src/utils/ @bitwarden/team-platform-dev
|
apps/web/src/utils/ @bitwarden/team-platform-dev
|
||||||
# Web core and shared files
|
# Web core and shared files
|
||||||
|
|||||||
@@ -1,119 +1 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
export { FakeStorageService } from "@bitwarden/storage-test-utils";
|
||||||
import { Subject } from "rxjs";
|
|
||||||
|
|
||||||
import {
|
|
||||||
AbstractStorageService,
|
|
||||||
ObservableStorageService,
|
|
||||||
StorageUpdate,
|
|
||||||
} from "../src/platform/abstractions/storage.service";
|
|
||||||
import { StorageOptions } from "../src/platform/models/domain/storage-options";
|
|
||||||
|
|
||||||
const INTERNAL_KEY = "__internal__";
|
|
||||||
|
|
||||||
export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
|
|
||||||
private store: Record<string, unknown>;
|
|
||||||
private updatesSubject = new Subject<StorageUpdate>();
|
|
||||||
private _valuesRequireDeserialization = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a mock of a {@see AbstractStorageService} for asserting the expected
|
|
||||||
* amount of calls. It is not recommended to use this to mock implementations as
|
|
||||||
* they are not respected.
|
|
||||||
*/
|
|
||||||
mock: MockProxy<AbstractStorageService>;
|
|
||||||
|
|
||||||
constructor(initial?: Record<string, unknown>) {
|
|
||||||
this.store = initial ?? {};
|
|
||||||
this.mock = mock<AbstractStorageService>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the internal store for this fake implementation, this bypasses any mock calls
|
|
||||||
* or updates to the {@link updates$} observable.
|
|
||||||
* @param store
|
|
||||||
*/
|
|
||||||
internalUpdateStore(store: Record<string, unknown>) {
|
|
||||||
this.store = store;
|
|
||||||
}
|
|
||||||
|
|
||||||
get internalStore() {
|
|
||||||
return this.store;
|
|
||||||
}
|
|
||||||
|
|
||||||
internalUpdateValuesRequireDeserialization(value: boolean) {
|
|
||||||
this._valuesRequireDeserialization = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get valuesRequireDeserialization(): boolean {
|
|
||||||
return this._valuesRequireDeserialization;
|
|
||||||
}
|
|
||||||
|
|
||||||
get updates$() {
|
|
||||||
return this.updatesSubject.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
get<T>(key: string, options?: StorageOptions): Promise<T> {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.mock.get(key, options);
|
|
||||||
const value = this.store[key] as T;
|
|
||||||
return Promise.resolve(value);
|
|
||||||
}
|
|
||||||
has(key: string, options?: StorageOptions): Promise<boolean> {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.mock.has(key, options);
|
|
||||||
return Promise.resolve(this.store[key] != null);
|
|
||||||
}
|
|
||||||
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
|
|
||||||
// These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203
|
|
||||||
// which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world.
|
|
||||||
if (typeof key !== "string" && typeof key !== "object") {
|
|
||||||
throw new TypeError(
|
|
||||||
`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't throw this error because ElectronStorageService automatically detects this case
|
|
||||||
// and calls `delete()` instead of `set()`.
|
|
||||||
// if (typeof key !== "object" && obj === undefined) {
|
|
||||||
// throw new TypeError("Use `delete()` to clear values");
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (this._containsReservedKey(key)) {
|
|
||||||
throw new TypeError(
|
|
||||||
`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.mock.save(key, obj, options);
|
|
||||||
this.store[key] = obj;
|
|
||||||
this.updatesSubject.next({ key: key, updateType: "save" });
|
|
||||||
}
|
|
||||||
remove(key: string, options?: StorageOptions): Promise<void> {
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.mock.remove(key, options);
|
|
||||||
delete this.store[key];
|
|
||||||
this.updatesSubject.next({ key: key, updateType: "remove" });
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _containsReservedKey(key: string | Partial<unknown>): boolean {
|
|
||||||
if (typeof key === "object") {
|
|
||||||
const firsKey = Object.keys(key)[0];
|
|
||||||
|
|
||||||
if (firsKey === INTERNAL_KEY) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof key !== "string") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
5
libs/storage-test-utils/README.md
Normal file
5
libs/storage-test-utils/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# storage-test-utils
|
||||||
|
|
||||||
|
Owned by: platform
|
||||||
|
|
||||||
|
Test tools for the storage library
|
||||||
3
libs/storage-test-utils/eslint.config.mjs
Normal file
3
libs/storage-test-utils/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import baseConfig from "../../eslint.config.mjs";
|
||||||
|
|
||||||
|
export default [...baseConfig];
|
||||||
10
libs/storage-test-utils/jest.config.js
Normal file
10
libs/storage-test-utils/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: "storage-test-utils",
|
||||||
|
preset: "../../jest.preset.js",
|
||||||
|
testEnvironment: "node",
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ["ts", "js", "html"],
|
||||||
|
coverageDirectory: "../../coverage/libs/storage-test-utils",
|
||||||
|
};
|
||||||
11
libs/storage-test-utils/package.json
Normal file
11
libs/storage-test-utils/package.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@bitwarden/storage-test-utils",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Test tools for the storage library",
|
||||||
|
"private": true,
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"license": "GPL-3.0",
|
||||||
|
"author": "platform"
|
||||||
|
}
|
||||||
33
libs/storage-test-utils/project.json
Normal file
33
libs/storage-test-utils/project.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "storage-test-utils",
|
||||||
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
|
"sourceRoot": "libs/storage-test-utils/src",
|
||||||
|
"projectType": "library",
|
||||||
|
"tags": [],
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"executor": "@nx/js:tsc",
|
||||||
|
"outputs": ["{options.outputPath}"],
|
||||||
|
"options": {
|
||||||
|
"outputPath": "dist/libs/storage-test-utils",
|
||||||
|
"main": "libs/storage-test-utils/src/index.ts",
|
||||||
|
"tsConfig": "libs/storage-test-utils/tsconfig.lib.json",
|
||||||
|
"assets": ["libs/storage-test-utils/*.md"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"executor": "@nx/eslint:lint",
|
||||||
|
"outputs": ["{options.outputFile}"],
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": ["libs/storage-test-utils/**/*.ts"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"executor": "@nx/jest:jest",
|
||||||
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
|
"options": {
|
||||||
|
"jestConfig": "libs/storage-test-utils/jest.config.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
119
libs/storage-test-utils/src/fake-storage.service.ts
Normal file
119
libs/storage-test-utils/src/fake-storage.service.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AbstractStorageService,
|
||||||
|
ObservableStorageService,
|
||||||
|
StorageUpdate,
|
||||||
|
StorageOptions,
|
||||||
|
} from "@bitwarden/storage-core";
|
||||||
|
|
||||||
|
const INTERNAL_KEY = "__internal__";
|
||||||
|
|
||||||
|
export class FakeStorageService implements AbstractStorageService, ObservableStorageService {
|
||||||
|
private store: Record<string, unknown>;
|
||||||
|
private updatesSubject = new Subject<StorageUpdate>();
|
||||||
|
private _valuesRequireDeserialization = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mock of a {@see AbstractStorageService} for asserting the expected
|
||||||
|
* amount of calls. It is not recommended to use this to mock implementations as
|
||||||
|
* they are not respected.
|
||||||
|
*/
|
||||||
|
mock: MockProxy<AbstractStorageService>;
|
||||||
|
|
||||||
|
constructor(initial?: Record<string, unknown>) {
|
||||||
|
this.store = initial ?? {};
|
||||||
|
this.mock = mock<AbstractStorageService>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the internal store for this fake implementation, this bypasses any mock calls
|
||||||
|
* or updates to the {@link updates$} observable.
|
||||||
|
* @param store
|
||||||
|
*/
|
||||||
|
internalUpdateStore(store: Record<string, unknown>) {
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
get internalStore() {
|
||||||
|
return this.store;
|
||||||
|
}
|
||||||
|
|
||||||
|
internalUpdateValuesRequireDeserialization(value: boolean) {
|
||||||
|
this._valuesRequireDeserialization = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get valuesRequireDeserialization(): boolean {
|
||||||
|
return this._valuesRequireDeserialization;
|
||||||
|
}
|
||||||
|
|
||||||
|
get updates$() {
|
||||||
|
return this.updatesSubject.asObservable();
|
||||||
|
}
|
||||||
|
|
||||||
|
get<T>(key: string, options?: StorageOptions): Promise<T> {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.mock.get(key, options);
|
||||||
|
const value = this.store[key] as T;
|
||||||
|
return Promise.resolve(value);
|
||||||
|
}
|
||||||
|
has(key: string, options?: StorageOptions): Promise<boolean> {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.mock.has(key, options);
|
||||||
|
return Promise.resolve(this.store[key] != null);
|
||||||
|
}
|
||||||
|
async save<T>(key: string, obj: T, options?: StorageOptions): Promise<void> {
|
||||||
|
// These exceptions are copied from https://github.com/sindresorhus/conf/blob/608adb0c46fb1680ddbd9833043478367a64c120/source/index.ts#L193-L203
|
||||||
|
// which is a library that is used by `ElectronStorageService`. We add them here to ensure that the behavior in our testing mirrors the real world.
|
||||||
|
if (typeof key !== "string" && typeof key !== "object") {
|
||||||
|
throw new TypeError(
|
||||||
|
`Expected \`key\` to be of type \`string\` or \`object\`, got ${typeof key}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't throw this error because ElectronStorageService automatically detects this case
|
||||||
|
// and calls `delete()` instead of `set()`.
|
||||||
|
// if (typeof key !== "object" && obj === undefined) {
|
||||||
|
// throw new TypeError("Use `delete()` to clear values");
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (this._containsReservedKey(key)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`Please don't use the ${INTERNAL_KEY} key, as it's used to manage this module internal operations.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.mock.save(key, obj, options);
|
||||||
|
this.store[key] = obj;
|
||||||
|
this.updatesSubject.next({ key: key, updateType: "save" });
|
||||||
|
}
|
||||||
|
remove(key: string, options?: StorageOptions): Promise<void> {
|
||||||
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
this.mock.remove(key, options);
|
||||||
|
delete this.store[key];
|
||||||
|
this.updatesSubject.next({ key: key, updateType: "remove" });
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _containsReservedKey(key: string | Partial<unknown>): boolean {
|
||||||
|
if (typeof key === "object") {
|
||||||
|
const firsKey = Object.keys(key)[0];
|
||||||
|
|
||||||
|
if (firsKey === INTERNAL_KEY) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/storage-test-utils/src/index.ts
Normal file
1
libs/storage-test-utils/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./fake-storage.service";
|
||||||
8
libs/storage-test-utils/src/storage-test-utils.spec.ts
Normal file
8
libs/storage-test-utils/src/storage-test-utils.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as lib from "./index";
|
||||||
|
|
||||||
|
describe("storage-test-utils", () => {
|
||||||
|
// This test will fail until something is exported from index.ts
|
||||||
|
it("should work", () => {
|
||||||
|
expect(lib).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
13
libs/storage-test-utils/tsconfig.json
Normal file
13
libs/storage-test-utils/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
10
libs/storage-test-utils/tsconfig.lib.json
Normal file
10
libs/storage-test-utils/tsconfig.lib.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"declaration": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
|
||||||
|
}
|
||||||
10
libs/storage-test-utils/tsconfig.spec.json
Normal file
10
libs/storage-test-utils/tsconfig.spec.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../..//dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node10",
|
||||||
|
"types": ["jest", "node"]
|
||||||
|
},
|
||||||
|
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -250,8 +250,6 @@
|
|||||||
},
|
},
|
||||||
"apps/cli/node_modules/is-docker": {
|
"apps/cli/node_modules/is-docker": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"is-docker": "cli.js"
|
"is-docker": "cli.js"
|
||||||
@@ -375,6 +373,11 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "GPL-3.0"
|
"license": "GPL-3.0"
|
||||||
},
|
},
|
||||||
|
"libs/storage-test-utils": {
|
||||||
|
"name": "@bitwarden/storage-test-utils",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "GPL-3.0"
|
||||||
|
},
|
||||||
"libs/tools/export/vault-export/vault-export-core": {
|
"libs/tools/export/vault-export/vault-export-core": {
|
||||||
"name": "@bitwarden/vault-export-core",
|
"name": "@bitwarden/vault-export-core",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -4621,6 +4624,10 @@
|
|||||||
"resolved": "libs/storage-core",
|
"resolved": "libs/storage-core",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@bitwarden/storage-test-utils": {
|
||||||
|
"resolved": "libs/storage-test-utils",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@bitwarden/ui-common": {
|
"node_modules/@bitwarden/ui-common": {
|
||||||
"resolved": "libs/ui/common",
|
"resolved": "libs/ui/common",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"@bitwarden/platform/*": ["./libs/platform/src/*"],
|
"@bitwarden/platform/*": ["./libs/platform/src/*"],
|
||||||
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
|
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
|
||||||
"@bitwarden/storage-core": ["libs/storage-core/src/index.ts"],
|
"@bitwarden/storage-core": ["libs/storage-core/src/index.ts"],
|
||||||
|
"@bitwarden/storage-test-utils": ["libs/storage-test-utils/src/index.ts"],
|
||||||
"@bitwarden/ui-common": ["./libs/ui/common/src"],
|
"@bitwarden/ui-common": ["./libs/ui/common/src"],
|
||||||
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
|
"@bitwarden/ui-common/setup-jest": ["./libs/ui/common/src/setup-jest"],
|
||||||
"@bitwarden/vault": ["./libs/vault/src"],
|
"@bitwarden/vault": ["./libs/vault/src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user