import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; import type { Expires } from '../../../../src/storage/keyvalue/WrappedExpiringStorage'; import { WrappedExpiringStorage } from '../../../../src/storage/keyvalue/WrappedExpiringStorage'; import { InternalServerError } from '../../../../src/util/errors/InternalServerError'; import clearAllTimers = jest.clearAllTimers; type Internal = Expires; function createExpires(payload: string, expires?: Date): Internal { return { payload, expires: expires?.toISOString() }; } jest.useFakeTimers(); describe('A WrappedExpiringStorage', (): void => { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); let source: jest.Mocked>; let storage: WrappedExpiringStorage; beforeEach(async(): Promise => { source = { get: jest.fn(), has: jest.fn(), set: jest.fn(), delete: jest.fn(), entries: jest.fn(), }; storage = new WrappedExpiringStorage(source); }); afterEach(async(): Promise => { clearAllTimers(); }); it('does not return data if there is no result.', async(): Promise => { await expect(storage.get('key')).resolves.toBeUndefined(); expect(source.get).toHaveBeenCalledTimes(1); expect(source.get).toHaveBeenLastCalledWith('key'); }); it('returns data if it has not expired.', async(): Promise => { source.get.mockResolvedValueOnce(createExpires('data!', tomorrow)); await expect(storage.get('key')).resolves.toBe('data!'); }); it('deletes expired data when trying to get it.', async(): Promise => { source.get.mockResolvedValueOnce(createExpires('data!', yesterday)); await expect(storage.get('key')).resolves.toBeUndefined(); expect(source.delete).toHaveBeenCalledTimes(1); expect(source.delete).toHaveBeenLastCalledWith('key'); }); it('returns false on `has` checks if there is no data.', async(): Promise => { await expect(storage.has('key')).resolves.toBe(false); expect(source.get).toHaveBeenCalledTimes(1); expect(source.get).toHaveBeenLastCalledWith('key'); }); it('true on `has` checks if there is non-expired data.', async(): Promise => { source.get.mockResolvedValueOnce(createExpires('data!', tomorrow)); await expect(storage.has('key')).resolves.toBe(true); }); it('deletes expired data when checking if it exists.', async(): Promise => { source.get.mockResolvedValueOnce(createExpires('data!', yesterday)); await expect(storage.has('key')).resolves.toBe(false); expect(source.delete).toHaveBeenCalledTimes(1); expect(source.delete).toHaveBeenLastCalledWith('key'); }); it('converts the expiry date to a string when storing data.', async(): Promise => { await storage.set('key', 'data!', tomorrow); expect(source.set).toHaveBeenCalledTimes(1); expect(source.set).toHaveBeenLastCalledWith('key', createExpires('data!', tomorrow)); }); it('can store data with an expiration duration.', async(): Promise => { await storage.set('key', 'data!', tomorrow.getTime() - Date.now()); expect(source.set).toHaveBeenCalledTimes(1); expect(source.set).toHaveBeenLastCalledWith('key', createExpires('data!', tomorrow)); }); it('can store data without expiry date.', async(): Promise => { await storage.set('key', 'data!'); expect(source.set).toHaveBeenCalledTimes(1); expect(source.set).toHaveBeenLastCalledWith('key', createExpires('data!')); }); it('errors when trying to store expired data.', async(): Promise => { await expect(storage.set('key', 'data!', yesterday)).rejects.toThrow(InternalServerError); }); it('directly calls delete on the source when deleting.', async(): Promise => { await expect(storage.delete('key')).resolves.toBeUndefined(); expect(source.delete).toHaveBeenCalledTimes(1); expect(source.delete).toHaveBeenLastCalledWith('key'); }); it('only iterates over non-expired entries.', async(): Promise => { const data = [ [ 'key1', createExpires('data1', tomorrow) ], [ 'key2', createExpires('data2', yesterday) ], [ 'key3', createExpires('data3') ], ]; source.entries.mockImplementationOnce(function* (): any { yield* data; }); const it = storage.entries(); await expect(it.next()).resolves.toEqual( expect.objectContaining({ value: [ 'key1', 'data1' ]}), ); await expect(it.next()).resolves.toEqual( expect.objectContaining({ value: [ 'key3', 'data3' ]}), ); }); it('removes expired entries after a given time.', async(): Promise => { // Disable interval function and simply check it was called with the correct parameters // Otherwise it gets quite difficult to verify the async interval function gets executed const mockInterval = jest.spyOn(global, 'setInterval'); mockInterval.mockImplementation(jest.fn()); // Timeout of 1 minute storage = new WrappedExpiringStorage(source, 1); const data = [ [ 'key1', createExpires('data1', tomorrow) ], [ 'key2', createExpires('data2', yesterday) ], [ 'key3', createExpires('data3') ], ]; source.entries.mockImplementationOnce(function* (): any { yield* data; }); // Make sure interval is created correctly expect(mockInterval.mock.calls).toHaveLength(1); expect(mockInterval.mock.calls[0]).toHaveLength(2); expect(mockInterval.mock.calls[0][1]).toBe(60 * 1000); // Await the function that should have been executed by the interval await (mockInterval.mock.calls[0][0] as () => Promise)(); expect(source.delete).toHaveBeenCalledTimes(1); expect(source.delete).toHaveBeenLastCalledWith('key2'); mockInterval.mockRestore(); }); it('can stop the timer.', async(): Promise => { const mockInterval = jest.spyOn(global, 'setInterval'); const mockClear = jest.spyOn(global, 'clearInterval'); // Timeout of 1 minute storage = new WrappedExpiringStorage(source, 1); const data = [ [ 'key1', createExpires('data1', tomorrow) ], [ 'key2', createExpires('data2', yesterday) ], [ 'key3', createExpires('data3') ], ]; source.entries.mockImplementationOnce(function* (): any { yield* data; }); await expect(storage.finalize()).resolves.toBeUndefined(); // Make sure clearInterval was called with the interval timer expect(mockClear.mock.calls).toHaveLength(1); expect(mockClear.mock.calls[0]).toHaveLength(1); expect(mockClear.mock.calls[0][0]).toBe(mockInterval.mock.results[0].value); mockInterval.mockRestore(); mockClear.mockRestore(); }); });