From f4afc3ce16807bbb98c9c2e07b924511514bdd91 Mon Sep 17 00:00:00 2001 From: smessie Date: Tue, 17 Nov 2020 18:07:31 +0100 Subject: [PATCH] test: Divide current tests into integration and unit tests for LockingResourceStore --- test/integration/LockingResourceStore.test.ts | 104 ++++++++++++++++++ .../unit/storage/LockingResourceStore.test.ts | 63 ++--------- .../WrappedExpiringResourceLocker.test.ts | 57 ++++++++++ 3 files changed, 168 insertions(+), 56 deletions(-) create mode 100644 test/integration/LockingResourceStore.test.ts diff --git a/test/integration/LockingResourceStore.test.ts b/test/integration/LockingResourceStore.test.ts new file mode 100644 index 000000000..73ffe3d74 --- /dev/null +++ b/test/integration/LockingResourceStore.test.ts @@ -0,0 +1,104 @@ +import streamifyArray from 'streamify-array'; +import { RepresentationMetadata } from '../../src/ldp/representation/RepresentationMetadata'; +import { InMemoryDataAccessor } from '../../src/storage/accessors/InMemoryDataAccessor'; +import { DataAccessorBasedStore } from '../../src/storage/DataAccessorBasedStore'; +import { LockingResourceStore } from '../../src/storage/LockingResourceStore'; +import type { ResourceStore } from '../../src/storage/ResourceStore'; +import { UrlContainerManager } from '../../src/storage/UrlContainerManager'; +import { APPLICATION_OCTET_STREAM } from '../../src/util/ContentTypes'; +import type { ExpiringResourceLocker } from '../../src/util/locking/ExpiringResourceLocker'; +import type { ResourceLocker } from '../../src/util/locking/ResourceLocker'; +import { SingleThreadedResourceLocker } from '../../src/util/locking/SingleThreadedResourceLocker'; +import { WrappedExpiringResourceLocker } from '../../src/util/locking/WrappedExpiringResourceLocker'; +import { MetadataController } from '../../src/util/MetadataController'; +import { CONTENT_TYPE } from '../../src/util/UriConstants'; + +describe('A LockingResourceStore', (): void => { + let path: string; + let store: LockingResourceStore; + let locker: ResourceLocker; + let expiringLocker: ExpiringResourceLocker; + let source: ResourceStore; + + beforeEach(async(): Promise => { + jest.clearAllMocks(); + + const base = 'http://test.com/'; + path = `${base}path`; + const metadataController = new MetadataController(); + const containerManager = new UrlContainerManager(base); + source = new DataAccessorBasedStore( + new InMemoryDataAccessor(base, metadataController), base, metadataController, containerManager, + ); + + locker = new SingleThreadedResourceLocker(); + expiringLocker = new WrappedExpiringResourceLocker(locker, 1000); + + store = new LockingResourceStore(source, expiringLocker); + + // Make sure something is in the store before we read from it in our tests. + const metadata = new RepresentationMetadata({ [CONTENT_TYPE]: APPLICATION_OCTET_STREAM }); + const data = streamifyArray([ 1, 2, 3 ]); + await store.setRepresentation({ path }, { metadata, data, binary: true }); + }); + + it('destroys the stream when nothing is read after 1000ms.', async(): Promise => { + jest.useFakeTimers(); + + // Spy on a real ResourceLocker and ResourceStore instance + const acquireSpy = jest.spyOn(expiringLocker, 'acquire'); + const getRepresentationSpy = jest.spyOn(source, 'getRepresentation'); + + const representation = await store.getRepresentation({ path }, {}); + const errorCallback = jest.fn(); + representation.data.on('error', errorCallback); + + // Wait 1000ms and read + jest.advanceTimersByTime(1000); + expect(representation.data.read()).toBeNull(); + + // Verify a timeout error was thrown + await new Promise((resolve): any => setImmediate(resolve)); + expect(errorCallback).toHaveBeenCalledTimes(1); + expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout exceeded')); + + // Verify the lock was acquired and released at the right time + expect(acquireSpy).toHaveBeenCalledTimes(1); + expect(acquireSpy).toHaveBeenLastCalledWith({ path }); + expect(getRepresentationSpy).toHaveBeenCalledTimes(1); + }); + + it('destroys the stream when pauses between reads exceed 1000ms.', async(): Promise => { + jest.useFakeTimers(); + + // Spy on a real ResourceLocker and ResourceStore instance + const acquireSpy = jest.spyOn(expiringLocker, 'acquire'); + const getRepresentationSpy = jest.spyOn(source, 'getRepresentation'); + + const representation = await store.getRepresentation({ path }, {}); + const errorCallback = jest.fn(); + representation.data.on('error', errorCallback); + + // Wait 750ms and read + jest.advanceTimersByTime(750); + expect(representation.data.read()).toBe(1); + + // Wait 750ms and read + jest.advanceTimersByTime(750); + expect(representation.data.read()).toBe(2); + + // Wait 1000ms and watch the stream be destroyed + jest.advanceTimersByTime(1000); + expect(representation.data.read()).toBeNull(); + + // Verify a timeout error was thrown + await new Promise((resolve): any => setImmediate(resolve)); + expect(errorCallback).toHaveBeenCalledTimes(1); + expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout exceeded')); + + // Verify the lock was acquired and released at the right time + expect(acquireSpy).toHaveBeenCalledTimes(1); + expect(acquireSpy).toHaveBeenLastCalledWith({ path }); + expect(getRepresentationSpy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/test/unit/storage/LockingResourceStore.test.ts b/test/unit/storage/LockingResourceStore.test.ts index b2d682162..d53da2d55 100644 --- a/test/unit/storage/LockingResourceStore.test.ts +++ b/test/unit/storage/LockingResourceStore.test.ts @@ -6,7 +6,6 @@ import { LockingResourceStore } from '../../../src/storage/LockingResourceStore' import type { ResourceStore } from '../../../src/storage/ResourceStore'; import type { ExpiringLock } from '../../../src/util/locking/ExpiringLock'; import type { ExpiringResourceLocker } from '../../../src/util/locking/ExpiringResourceLocker'; -import { WrappedExpiringResourceLocker } from '../../../src/util/locking/WrappedExpiringResourceLocker'; describe('A LockingResourceStore', (): void => { let store: LockingResourceStore; @@ -190,65 +189,17 @@ describe('A LockingResourceStore', (): void => { expect(order).toEqual([ 'acquire', 'getRepresentation', 'end', 'close', 'release' ]); }); - it('destroys the stream when nothing is read after 1000ms.', async(): Promise => { - jest.useFakeTimers(); - - // Spy on a real ResourceLocker instance - const expiringLocker = new WrappedExpiringResourceLocker(locker, 1000); - store = new LockingResourceStore(source, expiringLocker); - const acquireSpy = jest.spyOn(expiringLocker, 'acquire'); - + it('releases the lock on the resource when readable times out.', async(): Promise => { + // Make the representation time out const representation = await store.getRepresentation({ path: 'path' }, {}); - const errorCallback = jest.fn(); - representation.data.on('error', errorCallback); - - // Wait 1000ms and read - jest.advanceTimersByTime(1000); - expect(representation.data.read()).toBeNull(); + lock.emit('expired'); await registerEventOrder(representation.data, 'close'); - // Verify a timeout error was thrown - expect(errorCallback).toHaveBeenCalledTimes(1); - expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout exceeded')); - // Verify the lock was acquired and released at the right time - expect(acquireSpy).toHaveBeenCalledTimes(1); - expect(acquireSpy).toHaveBeenLastCalledWith({ path: 'path' }); - expect(source.getRepresentation).toHaveBeenCalledTimes(1); - }); - - it('destroys the stream when pauses between reads exceed 1000ms.', async(): Promise => { - jest.useFakeTimers(); - - // Spy on a real ResourceLocker instance - const expiringLocker = new WrappedExpiringResourceLocker(locker, 1000); - store = new LockingResourceStore(source, expiringLocker); - const acquireSpy = jest.spyOn(expiringLocker, 'acquire'); - - const representation = await store.getRepresentation({ path: 'path' }, {}); - const errorCallback = jest.fn(); - representation.data.on('error', errorCallback); - - // Wait 750ms and read - jest.advanceTimersByTime(750); - expect(representation.data.read()).toBe(1); - - // Wait 750ms and read - jest.advanceTimersByTime(750); - expect(representation.data.read()).toBe(2); - - // Wait 1000ms and watch the stream be destroyed - jest.advanceTimersByTime(1000); - expect(representation.data.read()).toBeNull(); - await registerEventOrder(representation.data, 'close'); - - // Verify a timeout error was thrown - expect(errorCallback).toHaveBeenCalledTimes(1); - expect(errorCallback).toHaveBeenLastCalledWith(new Error('Stream reading timout exceeded')); - - // Verify the lock was acquired and released at the right time - expect(acquireSpy).toHaveBeenCalledTimes(1); - expect(acquireSpy).toHaveBeenLastCalledWith({ path: 'path' }); + expect(locker.acquire).toHaveBeenCalledTimes(1); + expect(locker.acquire).toHaveBeenLastCalledWith({ path: 'path' }); expect(source.getRepresentation).toHaveBeenCalledTimes(1); + expect(lock.release).toHaveBeenCalledTimes(1); + expect(order).toEqual([ 'acquire', 'getRepresentation', 'close', 'release' ]); }); }); diff --git a/test/unit/util/locking/WrappedExpiringResourceLocker.test.ts b/test/unit/util/locking/WrappedExpiringResourceLocker.test.ts index ebdb13aa9..99b30a042 100644 --- a/test/unit/util/locking/WrappedExpiringResourceLocker.test.ts +++ b/test/unit/util/locking/WrappedExpiringResourceLocker.test.ts @@ -1,6 +1,23 @@ +import type { EventEmitter } from 'events'; +import streamifyArray from 'streamify-array'; import { WrappedExpiringResourceLocker } from '../../../../src/util/locking/WrappedExpiringResourceLocker'; describe('A WrappedExpiringResourceLocker', (): void => { + let order: string[]; + + beforeEach(async(): Promise => { + order = []; + }); + + const registerEventOrder = async(eventSource: EventEmitter, event: string): Promise => { + await new Promise((resolve): any => { + eventSource.prependListener(event, (): any => { + order.push(event); + resolve(); + }); + }); + }; + it('emits an error event when releasing the lock errors.', async(): Promise => { jest.useFakeTimers(); @@ -27,4 +44,44 @@ describe('A WrappedExpiringResourceLocker', (): void => { expect(errorCallback).toHaveBeenCalledTimes(1); expect(errorCallback).toHaveBeenLastCalledWith(new Error('Release error')); }); + + it('releases the lock on the resource when data has been read.', async(): Promise => { + // Mock the inner ResourceLocker. + const release = jest.fn(async(): Promise => order.push('release')); + const lock = { release }; + const locker = { + acquire: jest.fn(async(): Promise => { + order.push('acquire'); + return lock; + }), + }; + + const expiringLocker = new WrappedExpiringResourceLocker(locker, 1000); + const expiringLock = await expiringLocker.acquire({} as any); + + // Mimic the behavior of a LockingResourceStore to test the expiringLock methods called. + const source = streamifyArray([ 1, 2, 3 ]); + // eslint-disable-next-line jest/valid-expect-in-promise + new Promise((resolve): void => { + source.on('end', resolve); + source.on('close', resolve); + }).then((): any => expiringLock.release(), null); + const readable = Object.create(source, { + read: { + value(size: number): any { + expiringLock.renew(); + return source.read(size); + }, + }, + }); + + // Read all data from the "representation" + readable.on('data', (): any => true); + await registerEventOrder(readable, 'end'); + + // Verify the lock was acquired and released at the right time + expect(locker.acquire).toHaveBeenCalledTimes(1); + expect(lock.release).toHaveBeenCalledTimes(1); + expect(order).toEqual([ 'acquire', 'end', 'release' ]); + }); });