diff --git a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts index f8dd85c05c..d81cba48d2 100644 --- a/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts +++ b/packages/core-app-api/src/apis/implementations/StorageApi/WebStorage.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { WebStorage } from './WebStorage'; import { ErrorApi, StorageApi } from '@backstage/core-plugin-api'; @@ -33,6 +34,12 @@ describe('WebStorage Storage API', () => { const storage = createWebStorage(); expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); }); it('should allow the setting and getting of the simple data structures', async () => { @@ -44,9 +51,27 @@ describe('WebStorage Storage API', () => { expect(storage.get('myfakekey')).toBe('helloimastring'); expect(storage.get('mysecondfakekey')).toBe(1234); expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: 'helloimastring', + newValue: 'helloimastring', + }); + expect(storage.snapshot('mysecondfakekey')).toEqual({ + key: 'mysecondfakekey', + presence: 'present', + value: 1234, + newValue: 1234, + }); + expect(storage.snapshot('mythirdfakekey')).toEqual({ + key: 'mythirdfakekey', + presence: 'present', + value: true, + newValue: true, + }); }); - it('should allow setting of complex datastructures', async () => { + it('should allow setting of complex data structures', async () => { const storage = createWebStorage(); const mockData = { @@ -57,6 +82,12 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: mockData, + newValue: mockData, + }); }); it('should subscribe to key changes when setting a new value', async () => { @@ -67,10 +98,12 @@ describe('WebStorage Storage API', () => { const mockData = { hello: 'im a great new value' }; await new Promise(resolve => { - storage.observe$('correctKey').subscribe({ - next: (...args) => { - selectedKeyNextHandler(...args); - resolve(); + storage.observe$('correctKey').subscribe({ + next: snapshot => { + selectedKeyNextHandler(snapshot); + if (snapshot.presence === 'present') { + resolve(); + } }, }); @@ -79,10 +112,12 @@ describe('WebStorage Storage API', () => { storage.set('correctKey', mockData); }); - expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0); expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); expect(selectedKeyNextHandler).toHaveBeenCalledWith({ key: 'correctKey', + presence: 'present', + value: mockData, newValue: mockData, }); }); @@ -98,9 +133,11 @@ describe('WebStorage Storage API', () => { await new Promise(resolve => { storage.observe$('correctKey').subscribe({ - next: (...args) => { - selectedKeyNextHandler(...args); - resolve(); + next: snapshot => { + selectedKeyNextHandler(snapshot); + if (snapshot.presence === 'absent') { + resolve(); + } }, }); @@ -109,10 +146,12 @@ describe('WebStorage Storage API', () => { storage.remove('correctKey'); }); - expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(wrongKeyNextHandler).toHaveBeenCalledTimes(0); expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); expect(selectedKeyNextHandler).toHaveBeenCalledWith({ key: 'correctKey', + presence: 'absent', + value: undefined, newValue: undefined, }); }); @@ -130,9 +169,24 @@ describe('WebStorage Storage API', () => { expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); expect(firstStorage.get(keyName)).toBe('boop'); expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toEqual( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'boop', + newValue: 'boop', + }); + expect(secondStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'deerp', + newValue: 'deerp', + }); }); - it('should not clash with other namesapces when creating buckets', async () => { + it('should not clash with other namespaces when creating buckets', async () => { const rootStorage = createWebStorage(); // when getting key test2 it will translate to /profile/something/deep/test2 @@ -145,7 +199,9 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', { error: true }); - expect(secondStorage.get('deep/test2')).toBe(undefined); + expect(secondStorage.snapshot('deep/test2')).toMatchObject({ + presence: 'absent', + }); }); it('should call the error api when the json can not be parsed in local storage', async () => { @@ -155,9 +211,14 @@ describe('WebStorage Storage API', () => { localStorage.setItem('/Test/Mock/Thing/key', '{smd: asdouindA}'); - const value = rootStorage.get('key'); + const value = rootStorage.snapshot('key'); - expect(value).toBe(undefined); + expect(value).toEqual({ + key: 'key', + presence: 'absent', + value: undefined, + newValue: undefined, + }); expect(mockErrorApi.post).toHaveBeenCalledWith(expect.any(Error)); expect(mockErrorApi.post).toHaveBeenCalledWith( expect.objectContaining({ @@ -166,11 +227,35 @@ describe('WebStorage Storage API', () => { ); }); - it('should return a singleton for the same namespace and same bucket', async () => { + it('should return a stable reference for the same namespace and same bucket', async () => { const rootStorage = createWebStorage({ namespace: '/Test/Mock/Thing/Thing ', }); expect(rootStorage.forBucket('test')).toBe(rootStorage.forBucket('test')); }); + + it('should freeze the snapshot value', async () => { + const storage = createWebStorage(); + + const data = { foo: 'bar', baz: [{ foo: 'bar' }] }; + storage.set('foo', data); + + const snapshot = storage.snapshot('foo'); + expect(snapshot.value).not.toBe(data); + + if (snapshot.presence !== 'present') { + throw new Error('Invalid presence'); + } + + expect(() => { + snapshot.value.foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz[0].foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz.push({ foo: 'buzz' }); + }).toThrow(/Cannot add property 1, object is not extensible/); + }); });