From 154d98168498a786368f75a01f51b902b8958ee8 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Mon, 18 Sep 2023 09:45:34 +0200 Subject: [PATCH] feat: Split up EncodingPathStorage functionality into different classes --- RELEASE_NOTES.md | 10 ++- config/http/notifications/base/storage.json | 9 ++- .../handler/account-store/default.json | 18 +++-- .../handler/adapter-factory/webid.json | 9 ++- .../interaction/routes/credentials.json | 9 ++- config/identity/handler/jwks/default.json | 9 ++- config/identity/ownership/token.json | 9 ++- .../storage/key-value/storages/storages.json | 18 +++-- src/index.ts | 6 +- src/storage/keyvalue/Base64EncodingStorage.ts | 20 ++++++ src/storage/keyvalue/ContainerPathStorage.ts | 33 +++++++++ src/storage/keyvalue/EncodingPathStorage.ts | 68 ------------------- ...gPathStorage.ts => HashEncodingStorage.ts} | 23 ++++--- .../keyvalue/PassthroughKeyValueStorage.ts | 60 ++++++++++++++++ .../keyvalue/Base64EncodingStorage.test.ts | 40 +++++++++++ ...e.test.ts => ContainerPathStorage.test.ts} | 20 +++--- ...ge.test.ts => HashEncodingStorage.test.ts} | 11 ++- .../PassthroughKeyValueStorage.test.ts | 62 +++++++++++++++++ 18 files changed, 309 insertions(+), 125 deletions(-) create mode 100644 src/storage/keyvalue/Base64EncodingStorage.ts create mode 100644 src/storage/keyvalue/ContainerPathStorage.ts delete mode 100644 src/storage/keyvalue/EncodingPathStorage.ts rename src/storage/keyvalue/{HashEncodingPathStorage.ts => HashEncodingStorage.ts} (61%) create mode 100644 src/storage/keyvalue/PassthroughKeyValueStorage.ts create mode 100644 test/unit/storage/keyvalue/Base64EncodingStorage.test.ts rename test/unit/storage/keyvalue/{EncodingPathStorage.test.ts => ContainerPathStorage.test.ts} (64%) rename test/unit/storage/keyvalue/{HashEncodingPathStorage.test.ts => HashEncodingStorage.test.ts} (71%) create mode 100644 test/unit/storage/keyvalue/PassthroughKeyValueStorage.test.ts diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index f301a4d9c..232f6a041 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -23,15 +23,23 @@ The following changes pertain to the imports in the default configs: - There is a new `static-root.json` import option for `app/init`, setting a static page for the root container. -The following changes are relevant for v5 custom configs that replaced certain features. +The following changes are relevant for v6 custom configs that replaced certain features. - `/app/init/*` imports have changed. Functionality remained the same though. +- All imports that define storages have been updated with new storage classes. + - `/http/notifications/base/storage.json` + - `/identity/*` + - `/storage/keyvalue/storages/storages.json` ### Interface changes These changes are relevant if you wrote custom modules for the server that depend on existing interfaces. - The `AppRunner` functions to create and start the server now take a singular arguments object as input. +- Most of the key/value related storages had their constructors changed to allow more values. +- `EncodingPathStorage` has been removed + and its functionality split up over `Base64EncodingStorage` and `ContainerPathStorage`. + `HashEncodingPathStorage` has similarly been replaced by introducing `HashEncodingStorage`. ## v6.0.0 diff --git a/config/http/notifications/base/storage.json b/config/http/notifications/base/storage.json index 0c5bdc7ab..1360ce6a7 100644 --- a/config/http/notifications/base/storage.json +++ b/config/http/notifications/base/storage.json @@ -7,9 +7,12 @@ "@type": "KeyValueChannelStorage", "locker": { "@id": "urn:solid-server:default:ResourceLocker" }, "storage": { - "@type": "EncodingPathStorage", - "relativePath": "/notifications/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/notifications/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } } ] diff --git a/config/identity/handler/account-store/default.json b/config/identity/handler/account-store/default.json index 1d8297675..0ae3a0957 100644 --- a/config/identity/handler/account-store/default.json +++ b/config/identity/handler/account-store/default.json @@ -7,9 +7,12 @@ "@type": "BaseAccountStore", "saltRounds": 10, "storage": { - "@type": "EncodingPathStorage", - "relativePath": "/accounts/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/accounts/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } }, "forgotPasswordStorage": { "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage" @@ -20,9 +23,12 @@ "@id": "urn:solid-server:default:ExpiringForgotPasswordStorage", "@type": "WrappedExpiringStorage", "source": { - "@type": "EncodingPathStorage", - "relativePath": "/forgot-password/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/forgot-password/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } } ] diff --git a/config/identity/handler/adapter-factory/webid.json b/config/identity/handler/adapter-factory/webid.json index 33413979b..59d378735 100644 --- a/config/identity/handler/adapter-factory/webid.json +++ b/config/identity/handler/adapter-factory/webid.json @@ -12,9 +12,12 @@ "source": { "@type": "ExpiringAdapterFactory", "storage": { - "@type": "EncodingPathStorage", - "relativePath": "/idp/adapter/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/idp/adapter/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } } } diff --git a/config/identity/handler/interaction/routes/credentials.json b/config/identity/handler/interaction/routes/credentials.json index 2c100e138..373e4c5ac 100644 --- a/config/identity/handler/interaction/routes/credentials.json +++ b/config/identity/handler/interaction/routes/credentials.json @@ -4,9 +4,12 @@ { "comment": "Stores all client credential tokens.", "@id": "urn:solid-server:auth:password:CredentialsStorage", - "@type": "EncodingPathStorage", - "relativePath": "/accounts/credentials/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/accounts/credentials/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } }, { "comment": "Handles credential tokens. These can be used to automate clients. See documentation for more info.", diff --git a/config/identity/handler/jwks/default.json b/config/identity/handler/jwks/default.json index b6098fe45..fe7e54f85 100644 --- a/config/identity/handler/jwks/default.json +++ b/config/identity/handler/jwks/default.json @@ -11,9 +11,12 @@ }, { "@id": "urn:solid-server:default:KeyStorage", - "@type": "EncodingPathStorage", - "relativePath": "/idp/keys/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/idp/keys/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } ] } diff --git a/config/identity/ownership/token.json b/config/identity/ownership/token.json index e2e9f0006..2720d005c 100644 --- a/config/identity/ownership/token.json +++ b/config/identity/ownership/token.json @@ -13,9 +13,12 @@ "@id": "urn:solid-server:default:ExpiringTokenStorage", "@type": "WrappedExpiringStorage", "source": { - "@type": "EncodingPathStorage", - "relativePath": "/idp/tokens/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/idp/tokens/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } } ] diff --git a/config/storage/key-value/storages/storages.json b/config/storage/key-value/storages/storages.json index 83c8d5389..247179976 100644 --- a/config/storage/key-value/storages/storages.json +++ b/config/storage/key-value/storages/storages.json @@ -4,16 +4,22 @@ { "comment": "Used for internal storage by the locker.", "@id": "urn:solid-server:default:LockStorage", - "@type": "HashEncodingPathStorage", - "relativePath": "/locks/", - "source": { "@id": "urn:solid-server:default:BackendKeyValueStorage" } + "@type": "HashEncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/locks/", + "source": { "@id": "urn:solid-server:default:BackendKeyValueStorage" } + } }, { "comment": "Storage used by setup components.", "@id": "urn:solid-server:default:SetupStorage", - "@type": "EncodingPathStorage", - "relativePath": "/setup/", - "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + "@type": "Base64EncodingStorage", + "source": { + "@type": "ContainerPathStorage", + "relativePath": "/setup/", + "source": { "@id": "urn:solid-server:default:KeyValueStorage" } + } } ] } diff --git a/src/index.ts b/src/index.ts index 94297f88a..248fb328d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -398,13 +398,15 @@ export * from './storage/conversion/RepresentationConverter'; export * from './storage/conversion/TypedRepresentationConverter'; // Storage/KeyValue -export * from './storage/keyvalue/EncodingPathStorage'; +export * from './storage/keyvalue/Base64EncodingStorage'; +export * from './storage/keyvalue/ContainerPathStorage'; export * from './storage/keyvalue/ExpiringStorage'; -export * from './storage/keyvalue/HashEncodingPathStorage'; +export * from './storage/keyvalue/HashEncodingStorage'; export * from './storage/keyvalue/JsonFileStorage'; export * from './storage/keyvalue/JsonResourceStorage'; export * from './storage/keyvalue/KeyValueStorage'; export * from './storage/keyvalue/MemoryMapStorage'; +export * from './storage/keyvalue/PassthroughKeyValueStorage'; export * from './storage/keyvalue/WrappedExpiringStorage'; // Storage/Mapping diff --git a/src/storage/keyvalue/Base64EncodingStorage.ts b/src/storage/keyvalue/Base64EncodingStorage.ts new file mode 100644 index 000000000..0af000ffa --- /dev/null +++ b/src/storage/keyvalue/Base64EncodingStorage.ts @@ -0,0 +1,20 @@ +import type { KeyValueStorage } from './KeyValueStorage'; +import { PassthroughKeyValueStorage } from './PassthroughKeyValueStorage'; + +/** + * Encodes the input key with base64 encoding, + * to make sure there are no invalid or special path characters. + */ +export class Base64EncodingStorage extends PassthroughKeyValueStorage { + public constructor(source: KeyValueStorage) { + super(source); + } + + protected toNewKey(key: string): string { + return Buffer.from(key).toString('base64'); + } + + protected toOriginalKey(key: string): string { + return Buffer.from(key, 'base64').toString('utf-8'); + } +} diff --git a/src/storage/keyvalue/ContainerPathStorage.ts b/src/storage/keyvalue/ContainerPathStorage.ts new file mode 100644 index 000000000..bf0acd01d --- /dev/null +++ b/src/storage/keyvalue/ContainerPathStorage.ts @@ -0,0 +1,33 @@ +import { ensureTrailingSlash, joinUrl } from '../../util/PathUtil'; +import type { KeyValueStorage } from './KeyValueStorage'; +import { PassthroughKeyValueStorage } from './PassthroughKeyValueStorage'; + +/** + * A {@link KeyValueStorage} that prepends a relative path to the key. + */ +export class ContainerPathStorage extends PassthroughKeyValueStorage { + protected readonly basePath: string; + + public constructor(source: KeyValueStorage, relativePath: string) { + super(source); + this.basePath = ensureTrailingSlash(relativePath); + } + + public async* entries(): AsyncIterableIterator<[string, T]> { + for await (const [ key, value ] of this.source.entries()) { + // The only relevant entries for this storage are those that start with the base path + if (!key.startsWith(this.basePath)) { + continue; + } + yield [ this.toOriginalKey(key), value ]; + } + } + + protected toNewKey(key: string): string { + return joinUrl(this.basePath, key); + } + + protected toOriginalKey(path: string): string { + return path.slice(this.basePath.length); + } +} diff --git a/src/storage/keyvalue/EncodingPathStorage.ts b/src/storage/keyvalue/EncodingPathStorage.ts deleted file mode 100644 index da79e351e..000000000 --- a/src/storage/keyvalue/EncodingPathStorage.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ensureTrailingSlash, joinUrl } from '../../util/PathUtil'; -import type { KeyValueStorage } from './KeyValueStorage'; - -/** - * Transforms the keys into relative paths, to be used by the source storage. - * Encodes the input key with base64 encoding, - * to make sure there are no invalid or special path characters, - * and prepends it with the stored relative path. - * This can be useful to eventually generate URLs in specific containers - * without having to worry about cleaning the input keys. - */ -export class EncodingPathStorage implements KeyValueStorage { - protected readonly basePath: string; - protected readonly source: KeyValueStorage; - - public constructor(relativePath: string, source: KeyValueStorage) { - this.source = source; - this.basePath = ensureTrailingSlash(relativePath); - } - - public async get(key: string): Promise { - const path = this.keyToPath(key); - return this.source.get(path); - } - - public async has(key: string): Promise { - const path = this.keyToPath(key); - return this.source.has(path); - } - - public async set(key: string, value: T): Promise { - const path = this.keyToPath(key); - await this.source.set(path, value); - return this; - } - - public async delete(key: string): Promise { - const path = this.keyToPath(key); - return this.source.delete(path); - } - - public async* entries(): AsyncIterableIterator<[string, T]> { - for await (const [ path, value ] of this.source.entries()) { - // The only relevant entries for this storage are those that start with the base path - if (!path.startsWith(this.basePath)) { - continue; - } - const key = this.pathToKey(path); - yield [ key, value ]; - } - } - - /** - * Converts a key into a path for internal storage. - */ - protected keyToPath(key: string): string { - const encodedKey = Buffer.from(key).toString('base64'); - return joinUrl(this.basePath, encodedKey); - } - - /** - * Converts an internal storage path string into the original path key. - */ - protected pathToKey(path: string): string { - const buffer = Buffer.from(path.slice(this.basePath.length), 'base64'); - return buffer.toString('utf-8'); - } -} diff --git a/src/storage/keyvalue/HashEncodingPathStorage.ts b/src/storage/keyvalue/HashEncodingStorage.ts similarity index 61% rename from src/storage/keyvalue/HashEncodingPathStorage.ts rename to src/storage/keyvalue/HashEncodingStorage.ts index 87e2d1f9e..55f50c1aa 100644 --- a/src/storage/keyvalue/HashEncodingPathStorage.ts +++ b/src/storage/keyvalue/HashEncodingStorage.ts @@ -1,11 +1,12 @@ import { createHash } from 'crypto'; import { getLoggerFor } from '../../logging/LogUtil'; import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; -import { joinUrl } from '../../util/PathUtil'; -import { EncodingPathStorage } from './EncodingPathStorage'; +import type { KeyValueStorage } from './KeyValueStorage'; +import { PassthroughKeyValueStorage } from './PassthroughKeyValueStorage'; /** - * A variant of the {@link EncodingPathStorage} that hashes the key instead of converting to base64 encoding. + * Encodes the input key with SHA-256 hashing, + * to make sure there are no invalid or special path characters. * * This class was created specifically to prevent the issue of identifiers being too long when storing data: * https://github.com/CommunitySolidServer/CommunitySolidServer/issues/1013 @@ -13,16 +14,20 @@ import { EncodingPathStorage } from './EncodingPathStorage'; * This should eventually be replaced by a more structural approach once internal storage has been refactored * and data migration from older versions and formats is supported. */ -export class HashEncodingPathStorage extends EncodingPathStorage { +export class HashEncodingStorage extends PassthroughKeyValueStorage { protected readonly logger = getLoggerFor(this); - protected keyToPath(key: string): string { - const hash = createHash('sha256').update(key).digest('hex'); - this.logger.debug(`Hashing key ${key} to ${hash}`); - return joinUrl(this.basePath, hash); + public constructor(source: KeyValueStorage) { + super(source); } - protected pathToKey(): string { + protected toNewKey(key: string): string { + const hash = createHash('sha256').update(key).digest('hex'); + this.logger.debug(`Hashing key ${key} to ${hash}`); + return hash; + } + + protected toOriginalKey(): string { throw new NotImplementedHttpError('Hash keys cannot be converted back.'); } } diff --git a/src/storage/keyvalue/PassthroughKeyValueStorage.ts b/src/storage/keyvalue/PassthroughKeyValueStorage.ts new file mode 100644 index 000000000..872fdab83 --- /dev/null +++ b/src/storage/keyvalue/PassthroughKeyValueStorage.ts @@ -0,0 +1,60 @@ +import type { KeyValueStorage } from './KeyValueStorage'; + +/** + * Abstract class to create a {@link KeyValueStorage} by wrapping around another one. + * + * Exposes abstract functions to modify the key before passing it to the the source storage. + */ +export abstract class PassthroughKeyValueStorage implements KeyValueStorage { + protected readonly source: KeyValueStorage; + + protected constructor(source: KeyValueStorage) { + this.source = source; + } + + public async get(key: string): Promise { + const path = this.toNewKey(key); + return this.source.get(path); + } + + public async has(key: string): Promise { + const path = this.toNewKey(key); + return this.source.has(path); + } + + public async set(key: string, value: TVal): Promise { + const path = this.toNewKey(key); + await this.source.set(path, value); + return this; + } + + public async delete(key: string): Promise { + const path = this.toNewKey(key); + return this.source.delete(path); + } + + public async* entries(): AsyncIterableIterator<[string, TVal]> { + for await (const [ path, value ] of this.source.entries()) { + const key = this.toOriginalKey(path); + yield [ key, value ]; + } + } + + /** + * This function will be called on the input key and used as a new key when calling the source. + * + * @param key - Original input key. + * + * @returns A new key to use with the source storage. + */ + protected abstract toNewKey(key: string): string; + + /** + * This function is used when calling `entries()` to revert the key generated by `toNewKey()`. + * + * @param key - A key generated by `toNewKey()` + * + * @returns The original key. + */ + protected abstract toOriginalKey(key: string): string; +} diff --git a/test/unit/storage/keyvalue/Base64EncodingStorage.test.ts b/test/unit/storage/keyvalue/Base64EncodingStorage.test.ts new file mode 100644 index 000000000..f8bae0df7 --- /dev/null +++ b/test/unit/storage/keyvalue/Base64EncodingStorage.test.ts @@ -0,0 +1,40 @@ +import { Base64EncodingStorage } from '../../../../src/storage/keyvalue/Base64EncodingStorage'; +import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; + +describe('A Base64EncodingStorage', (): void => { + let map: Map; + let source: KeyValueStorage; + let storage: Base64EncodingStorage; + + beforeEach(async(): Promise => { + map = new Map(); + source = map as any; + storage = new Base64EncodingStorage(source); + }); + + it('encodes the keys.', async(): Promise => { + const key = 'key'; + // Base 64 encoding of 'key' + const encodedKey = 'a2V5'; + const data = 'data'; + await storage.set(key, data); + expect(map.size).toBe(1); + expect(map.get(encodedKey)).toBe(data); + await expect(storage.get(key)).resolves.toBe(data); + }); + + it('decodes the keys.', async(): Promise => { + // Base 64 encoding of 'key' + const encodedKey = 'a2V5'; + const data = 'data'; + + map.set(encodedKey, data); + + const results = []; + for await (const entry of storage.entries()) { + results.push(entry); + } + expect(results).toHaveLength(1); + expect(results[0]).toEqual([ 'key', data ]); + }); +}); diff --git a/test/unit/storage/keyvalue/EncodingPathStorage.test.ts b/test/unit/storage/keyvalue/ContainerPathStorage.test.ts similarity index 64% rename from test/unit/storage/keyvalue/EncodingPathStorage.test.ts rename to test/unit/storage/keyvalue/ContainerPathStorage.test.ts index 6e1c7abe5..396680bc0 100644 --- a/test/unit/storage/keyvalue/EncodingPathStorage.test.ts +++ b/test/unit/storage/keyvalue/ContainerPathStorage.test.ts @@ -1,23 +1,21 @@ -import { EncodingPathStorage } from '../../../../src/storage/keyvalue/EncodingPathStorage'; +import { ContainerPathStorage } from '../../../../src/storage/keyvalue/ContainerPathStorage'; import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; -describe('An EncodingPathStorage', (): void => { +describe('An ContainerPathStorage', (): void => { const relativePath = '/container/'; let map: Map; let source: KeyValueStorage; - let storage: EncodingPathStorage; + let storage: ContainerPathStorage; beforeEach(async(): Promise => { map = new Map(); source = map as any; - storage = new EncodingPathStorage(relativePath, source); + storage = new ContainerPathStorage(source, relativePath); }); - it('encodes the input key and joins it with the relativePath to create a new key.', async(): Promise => { + it('joins the input key with the relativePath to create a new key.', async(): Promise => { const key = 'key'; - // Base 64 encoding of 'key' - const encodedKey = 'a2V5'; - const generatedPath = `${relativePath}${encodedKey}`; + const generatedPath = `${relativePath}${key}`; const data = 'data'; await expect(storage.set(key, data)).resolves.toBe(storage); @@ -32,10 +30,8 @@ describe('An EncodingPathStorage', (): void => { }); it('only returns entries from the source storage matching the relative path.', async(): Promise => { - // Base 64 encoding of 'key' - const encodedKey = 'a2V5'; - const generatedPath = `${relativePath}${encodedKey}`; - const otherPath = `/otherContainer/${encodedKey}`; + const generatedPath = `${relativePath}key`; + const otherPath = `/otherContainer/key`; const data = 'data'; map.set(generatedPath, data); diff --git a/test/unit/storage/keyvalue/HashEncodingPathStorage.test.ts b/test/unit/storage/keyvalue/HashEncodingStorage.test.ts similarity index 71% rename from test/unit/storage/keyvalue/HashEncodingPathStorage.test.ts rename to test/unit/storage/keyvalue/HashEncodingStorage.test.ts index 41583ba30..c21b90965 100644 --- a/test/unit/storage/keyvalue/HashEncodingPathStorage.test.ts +++ b/test/unit/storage/keyvalue/HashEncodingStorage.test.ts @@ -1,17 +1,16 @@ -import { HashEncodingPathStorage } from '../../../../src/storage/keyvalue/HashEncodingPathStorage'; +import { HashEncodingStorage } from '../../../../src/storage/keyvalue/HashEncodingStorage'; import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; -describe('A HashEncodingPathStorage', (): void => { - const relativePath = '/container/'; +describe('A HashEncodingStorage', (): void => { let map: Map; let source: KeyValueStorage; - let storage: HashEncodingPathStorage; + let storage: HashEncodingStorage; beforeEach(async(): Promise => { map = new Map(); source = map as any; - storage = new HashEncodingPathStorage(relativePath, source); + storage = new HashEncodingStorage(source); }); it('hashes the keys.', async(): Promise => { @@ -20,7 +19,7 @@ describe('A HashEncodingPathStorage', (): void => { const data = 'data'; await storage.set(key, data); expect(map.size).toBe(1); - expect(map.get(`${relativePath}${hash}`)).toBe(data); + expect(map.get(hash)).toBe(data); await expect(storage.get(key)).resolves.toBe(data); }); diff --git a/test/unit/storage/keyvalue/PassthroughKeyValueStorage.test.ts b/test/unit/storage/keyvalue/PassthroughKeyValueStorage.test.ts new file mode 100644 index 000000000..b998e8021 --- /dev/null +++ b/test/unit/storage/keyvalue/PassthroughKeyValueStorage.test.ts @@ -0,0 +1,62 @@ +import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; +import { PassthroughKeyValueStorage } from '../../../../src/storage/keyvalue/PassthroughKeyValueStorage'; + +class DummyStorage extends PassthroughKeyValueStorage { + public constructor(source: KeyValueStorage) { + super(source); + } + + protected toNewKey(key: string): string { + return `dummy-${key}`; + } + + protected toOriginalKey(key: string): string { + return key.slice('dummy-'.length); + } +} + +describe('A PassthroughKeyValueStorage', (): void => { + let source: jest.Mocked>; + let storage: DummyStorage; + + beforeEach(async(): Promise => { + source = { + has: jest.fn(), + get: jest.fn(), + set: jest.fn(), + delete: jest.fn(), + entries: jest.fn(), + }; + + storage = new DummyStorage(source); + }); + + it('calls the source storage with the updated key.', async(): Promise => { + await storage.has('key'); + expect(source.has).toHaveBeenCalledTimes(1); + expect(source.has).toHaveBeenLastCalledWith('dummy-key'); + + await storage.get('key'); + expect(source.get).toHaveBeenCalledTimes(1); + expect(source.get).toHaveBeenLastCalledWith('dummy-key'); + + await storage.set('key', 'data'); + expect(source.set).toHaveBeenCalledTimes(1); + expect(source.set).toHaveBeenLastCalledWith('dummy-key', 'data'); + + await storage.delete('key'); + expect(source.delete).toHaveBeenCalledTimes(1); + expect(source.delete).toHaveBeenLastCalledWith('dummy-key'); + + // Set up data to test entries call + const map = new Map([[ 'dummy-key', 'value' ], [ 'dummy-key2', 'value2' ]]); + source.entries.mockReturnValue(map.entries() as unknown as AsyncIterableIterator<[string, string]>); + const results = []; + for await (const entry of storage.entries()) { + results.push(entry); + } + expect(results).toHaveLength(2); + expect(results[0]).toEqual([ 'key', 'value' ]); + expect(results[1]).toEqual([ 'key2', 'value2' ]); + }); +});