From 879bc21893f076a1386613dae6da10f4751f4b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 26 Jun 2020 09:41:28 +0200 Subject: [PATCH] feat(test-utils): mock storage api --- packages/test-utils/package.json | 3 +- .../apis/StorageApi/MockStorageApi.test.ts | 142 ++++++++++++++++++ .../apis/StorageApi/MockStorageApi.ts | 90 +++++++++++ .../src/testUtils/apis/StorageApi/index.ts | 18 +++ .../test-utils/src/testUtils/apis/index.ts | 1 + .../src/testUtils/mockApiRegistry.ts | 3 +- 6 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts create mode 100644 packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts create mode 100644 packages/test-utils/src/testUtils/apis/StorageApi/index.ts diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 501e288754..f0ed4905d5 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -41,7 +41,8 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-router": "^6.0.0-alpha.5", - "react-router-dom": "^6.0.0-alpha.5" + "react-router-dom": "^6.0.0-alpha.5", + "zen-observable": "^0.8.15" }, "devDependencies": { "@types/jest": "^25.2.2", diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts new file mode 100644 index 0000000000..668d973830 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MockStorageApi } from './MockStorageApi'; +import { StorageApi } from '@backstage/core-api'; + +describe('WebStorage Storage API', () => { + const createMockStorage = (): StorageApi => { + return MockStorageApi.create(); + }; + + it('should return undefined for values which are unset', async () => { + const storage = createMockStorage(); + + expect(storage.get('myfakekey')).toBeUndefined(); + }); + + it('should allow the setting and getting of the simple data structures', async () => { + const storage = createMockStorage(); + + await storage.set('myfakekey', 'helloimastring'); + await storage.set('mysecondfakekey', 1234); + await storage.set('mythirdfakekey', true); + expect(storage.get('myfakekey')).toBe('helloimastring'); + expect(storage.get('mysecondfakekey')).toBe(1234); + expect(storage.get('mythirdfakekey')).toBe(true); + }); + + it('should allow setting of complex datastructures', async () => { + const storage = createMockStorage(); + + const mockData = { + something: 'here', + is: [{ super: { complex: [{ but: 'something', why: true }] } }], + }; + + await storage.set('myfakekey', mockData); + + expect(storage.get('myfakekey')).toEqual(mockData); + }); + + it('should subscribe to key changes when setting a new value', async () => { + const storage = createMockStorage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.set('correctKey', mockData); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + newValue: mockData, + }); + }); + + it('should subscribe to key changes when deleting a value', async () => { + const storage = createMockStorage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + storage.set('correctKey', mockData); + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.remove('correctKey'); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + newValue: undefined, + }); + }); + + it('should be able to create different buckets for different uses', async () => { + const rootStorage = createMockStorage(); + + const firstStorage = rootStorage.forBucket('userSettings'); + const secondStorage = rootStorage.forBucket('profileSettings'); + const keyName = 'blobby'; + + await firstStorage.set(keyName, 'boop'); + await secondStorage.set(keyName, 'deerp'); + + expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); + expect(firstStorage.get(keyName)).toBe('boop'); + expect(secondStorage.get(keyName)).toBe('deerp'); + }); + + it('should not clash with other namesapces when creating buckets', async () => { + const rootStorage = createMockStorage(); + + // when getting key test2 it will translate to /profile/something/deep/test2 + const firstStorage = rootStorage + .forBucket('profile') + .forBucket('something') + .forBucket('deep'); + // when getting key deep/test2 it will translate to /profile/something/deep/test2 + const secondStorage = rootStorage.forBucket('profile/something'); + + await firstStorage.set('test2', { error: true }); + + expect(secondStorage.get('deep/test2')).toBe(undefined); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts new file mode 100644 index 0000000000..a3a4ef16d6 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Observable, + StorageApi, + storageApiRef, + StorageValueChange, +} from '@backstage/core-api'; +import ObservableImpl from 'zen-observable'; + +export type MockStorageBucket = { [key: string]: any }; + +export class MockStorageApi implements StorageApi { + static factory = { + implements: storageApiRef, + deps: {}, + factory: () => MockStorageApi.create(), + }; + + private readonly namespace: string; + private readonly data: MockStorageBucket; + + private constructor(namespace: string, data?: MockStorageBucket) { + this.namespace = namespace; + this.data = { ...data }; + } + + static create(data?: MockStorageBucket) { + return new MockStorageApi('', data); + } + + forBucket(name: string): StorageApi { + return new MockStorageApi(`${this.namespace}/${name}`, this.data); + } + + get(key: string): T | undefined { + return this.data[this.getKeyName(key)]; + } + + async set(key: string, data: T): Promise { + this.data[this.getKeyName(key)] = data; + this.notifyChanges({ key, newValue: data }); + } + + async remove(key: string): Promise { + delete this.data[this.getKeyName(key)]; + this.notifyChanges({ key, newValue: undefined }); + } + + observe$(key: string): Observable> { + return this.observable.filter(({ key: messageKey }) => messageKey === key); + } + + private getKeyName(key: string) { + return `${this.namespace}/${encodeURIComponent(key)}`; + } + + private notifyChanges(message: StorageValueChange) { + for (const subscription of this.subscribers) { + subscription.next(message); + } + } + + private subscribers = new Set< + ZenObservable.SubscriptionObserver + >(); + + private readonly observable = new ObservableImpl( + subscriber => { + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }, + ); +} diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/index.ts b/packages/test-utils/src/testUtils/apis/StorageApi/index.ts new file mode 100644 index 0000000000..ec4557b4c7 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/StorageApi/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MockStorageApi } from './MockStorageApi'; +export type { MockStorageBucket } from './MockStorageApi'; diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts index 55d6d10dd6..88229061de 100644 --- a/packages/test-utils/src/testUtils/apis/index.ts +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -15,3 +15,4 @@ */ export * from './ErrorApi'; +export * from './StorageApi'; diff --git a/packages/test-utils/src/testUtils/mockApiRegistry.ts b/packages/test-utils/src/testUtils/mockApiRegistry.ts index 96d12a9df2..15733ead88 100644 --- a/packages/test-utils/src/testUtils/mockApiRegistry.ts +++ b/packages/test-utils/src/testUtils/mockApiRegistry.ts @@ -15,12 +15,13 @@ */ import { ApiTestRegistry } from '@backstage/core-api'; -import { MockErrorApi } from './apis'; +import { MockErrorApi, MockStorageApi } from './apis'; export function createMockApiRegistry(): ApiTestRegistry { const registry = new ApiTestRegistry(); registry.register(MockErrorApi.factory); + registry.register(MockStorageApi.factory); return registry; }