diff --git a/src/index.ts b/src/index.ts index c918d2397..e8cd7b879 100644 --- a/src/index.ts +++ b/src/index.ts @@ -307,6 +307,7 @@ export * from './util/errors/UnsupportedMediaTypeHttpError'; // Util/Handlers export * from './util/handlers/AsyncHandler'; export * from './util/handlers/BooleanHandler'; +export * from './util/handlers/ConditionalHandler'; export * from './util/handlers/ParallelHandler'; export * from './util/handlers/SequenceHandler'; export * from './util/handlers/UnsupportedAsyncHandler'; diff --git a/src/util/handlers/ConditionalHandler.ts b/src/util/handlers/ConditionalHandler.ts new file mode 100644 index 000000000..5fd4ec624 --- /dev/null +++ b/src/util/handlers/ConditionalHandler.ts @@ -0,0 +1,57 @@ +import type { KeyValueStorage } from '../../storage/keyvalue/KeyValueStorage'; +import { NotImplementedHttpError } from '../errors/NotImplementedHttpError'; +import { AsyncHandler } from './AsyncHandler'; + +/** + * This handler will pass all requests to the wrapped handler, + * until a specific value has been set in the given storage. + * After that all input will be rejected. + * Once the value has been matched this behaviour will be cached, + * so changing the value again afterwards will not enable this handler again. + */ +export class ConditionalHandler extends AsyncHandler { + private readonly source: AsyncHandler; + private readonly storage: KeyValueStorage; + private readonly storageKey: string; + private readonly storageValue: unknown; + + private finished: boolean; + + public constructor(source: AsyncHandler, storage: KeyValueStorage, storageKey: string, + storageValue: unknown) { + super(); + this.source = source; + this.storage = storage; + this.storageKey = storageKey; + this.storageValue = storageValue; + + this.finished = false; + } + + public async canHandle(input: TIn): Promise { + await this.checkCondition(); + await this.source.canHandle(input); + } + + public async handleSafe(input: TIn): Promise { + await this.checkCondition(); + return this.source.handleSafe(input); + } + + public async handle(input: TIn): Promise { + return this.source.handle(input); + } + + /** + * Checks if the condition has already been fulfilled. + */ + private async checkCondition(): Promise { + if (!this.finished) { + this.finished = await this.storage.get(this.storageKey) === this.storageValue; + } + + if (this.finished) { + throw new NotImplementedHttpError('The condition has been fulfilled.'); + } + } +} diff --git a/test/unit/util/handlers/ConditionalHandler.test.ts b/test/unit/util/handlers/ConditionalHandler.test.ts new file mode 100644 index 000000000..5e6ef17dd --- /dev/null +++ b/test/unit/util/handlers/ConditionalHandler.test.ts @@ -0,0 +1,68 @@ +import type { KeyValueStorage } from '../../../../src/storage/keyvalue/KeyValueStorage'; +import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; +import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler'; +import { ConditionalHandler } from '../../../../src/util/handlers/ConditionalHandler'; + +describe('A ConditionalHandler', (): void => { + const storageKey = 'completed'; + const storageValue = true; + const input = 'input'; + let storage: KeyValueStorage; + let source: jest.Mocked>; + let handler: ConditionalHandler; + + beforeEach(async(): Promise => { + storage = new Map() as any; + source = { + canHandle: jest.fn(), + handleSafe: jest.fn().mockResolvedValue('handledSafely'), + handle: jest.fn().mockResolvedValue('handled'), + }; + + handler = new ConditionalHandler(source, storage, storageKey, storageValue); + }); + + it('send canHandle input to the source.', async(): Promise => { + await expect(handler.canHandle(input)).resolves.toBeUndefined(); + expect(source.canHandle).toHaveBeenCalledTimes(1); + expect(source.canHandle).toHaveBeenLastCalledWith(input); + }); + + it('rejects all canHandle requests once the storage value matches.', async(): Promise => { + await storage.set(storageKey, storageValue); + await expect(handler.canHandle(input)).rejects.toThrow(NotImplementedHttpError); + expect(source.canHandle).toHaveBeenCalledTimes(0); + }); + + it('caches the value of the storage.', async(): Promise => { + await storage.set(storageKey, storageValue); + await expect(handler.canHandle(input)).rejects.toThrow(NotImplementedHttpError); + await storage.delete(storageKey); + await expect(handler.canHandle(input)).rejects.toThrow(NotImplementedHttpError); + }); + + it('redirects input to the source handleSafe call.', async(): Promise => { + await expect(handler.handleSafe(input)).resolves.toBe('handledSafely'); + expect(source.handleSafe).toHaveBeenCalledTimes(1); + expect(source.handleSafe).toHaveBeenLastCalledWith(input); + }); + + it('rejects all handleSafe requests once the storage value matches.', async(): Promise => { + await storage.set(storageKey, storageValue); + await expect(handler.handleSafe(input)).rejects.toThrow(NotImplementedHttpError); + expect(source.handleSafe).toHaveBeenCalledTimes(0); + }); + + it('redirects input to the source handle call.', async(): Promise => { + await expect(handler.handle(input)).resolves.toBe('handled'); + expect(source.handle).toHaveBeenCalledTimes(1); + expect(source.handle).toHaveBeenLastCalledWith(input); + }); + + it('does not reject handle requests once the storage value matches.', async(): Promise => { + await storage.set(storageKey, storageValue); + await expect(handler.handle(input)).resolves.toBe('handled'); + expect(source.handle).toHaveBeenCalledTimes(1); + expect(source.handle).toHaveBeenLastCalledWith(input); + }); +});