feat: Use asynchandler library for handlers

This commit is contained in:
Joachim Van Herwegen
2024-09-05 11:08:43 +02:00
parent dce39f67e8
commit 58574eec07
186 changed files with 944 additions and 1590 deletions

View File

@@ -1,7 +1,7 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import type { ResourceIdentifier } from '../../../../src/http/representation/ResourceIdentifier';
import type { BaseRouterHandlerArgs } from '../../../../src/server/util/BaseRouterHandler';
import { BaseRouterHandler } from '../../../../src/server/util/BaseRouterHandler';
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
class SimpleRouterHandler extends BaseRouterHandler<AsyncHandler<{ method: string; target: ResourceIdentifier }>> {
public constructor(args: BaseRouterHandlerArgs<AsyncHandler<{ method: string; target: ResourceIdentifier }>>) {

View File

@@ -4,7 +4,9 @@ import {
createAggregateError,
errorTermsToMetadata,
extractErrorTerms,
toHttpError,
} from '../../../../src/util/errors/HttpErrorUtil';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import { toPredicateTerm } from '../../../../src/util/TermUtil';
describe('HttpErrorUtil', (): void => {
@@ -48,6 +50,23 @@ describe('HttpErrorUtil', (): void => {
});
});
describe('#toHttpError', (): void => {
it('keeps the error if it is an HttpError.', async(): Promise<void> => {
const error = new InternalServerError('internal');
expect(toHttpError(error)).toBe(error);
});
it('converts an error to an InternalServerError.', async(): Promise<void> => {
const error = new Error('error');
expect(toHttpError(error)).toEqual(new InternalServerError('error'));
});
it('converts unknown data to an InternalServerError.', async(): Promise<void> => {
const error = 'error';
expect(toHttpError(error)).toEqual(new InternalServerError('Unknown error: error'));
});
});
describe('#createAggregateError', (): void => {
const error401 = new HttpError(401, 'UnauthorizedHttpError');
const error415 = new HttpError(415, 'UnsupportedMediaTypeHttpError');

View File

@@ -1,40 +0,0 @@
import { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('An AsyncHandler', (): void => {
it('supports any input by default.', async(): Promise<void> => {
class EmptyHandler<T> extends AsyncHandler<T, null> {
public async handle(): Promise<null> {
return null;
}
}
const handler = new EmptyHandler<string>();
await expect(handler.canHandle('test')).resolves.toBeUndefined();
});
it('calls canHandle and handle when handleSafe is called.', async(): Promise<void> => {
const handlerTrue: AsyncHandler<any, any> = new StaticAsyncHandler(true, null);
const canHandleFn = jest.fn(async(input: any): Promise<void> => input);
const handleFn = jest.fn(async(input: any): Promise<any> => input);
handlerTrue.canHandle = canHandleFn;
handlerTrue.handle = handleFn;
await expect(handlerTrue.handleSafe('test')).resolves.toBe('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});
it('does not call handle when canHandle errors during a handleSafe call.', async(): Promise<void> => {
const handlerFalse: AsyncHandler<any, any> = new StaticAsyncHandler(false, null);
const canHandleFn = jest.fn(async(): Promise<void> => {
throw new Error('test');
});
const handleFn = jest.fn(async(input: any): Promise<any> => input);
handlerFalse.canHandle = canHandleFn;
handlerFalse.handle = handleFn;
await expect(handlerFalse.handleSafe('test')).rejects.toThrow(Error);
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(0);
});
});

View File

@@ -1,56 +0,0 @@
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { BooleanHandler } from '../../../../src/util/handlers/BooleanHandler';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A BooleanHandler', (): void => {
let handlerFalse: AsyncHandler<any, false>;
let handlerTrue: AsyncHandler<any, true>;
let handlerError: AsyncHandler<any, never>;
let handlerCanNotHandle: AsyncHandler<any, any>;
beforeEach(async(): Promise<void> => {
handlerFalse = new StaticAsyncHandler(true, false);
handlerTrue = new StaticAsyncHandler(true, true);
handlerError = new StaticAsyncHandler(true, null) as any;
handlerError.handle = (): never => {
throw new InternalServerError();
};
handlerCanNotHandle = new StaticAsyncHandler(false, null);
});
it('can handle the input if any of its handlers can.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerFalse, handlerCanNotHandle ]);
await expect(handler.canHandle(null)).resolves.toBeUndefined();
});
it('errors if none of its handlers supports the input.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerCanNotHandle, handlerCanNotHandle ]);
await expect(handler.canHandle(null)).rejects.toThrow('Not supported, Not supported');
});
it('returns true if any of its handlers returns true.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerFalse, handlerTrue, handlerCanNotHandle ]);
await expect(handler.handle(null)).resolves.toBe(true);
});
it('returns false if none of its handlers returns true.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerFalse, handlerError, handlerCanNotHandle ]);
await expect(handler.handle(null)).resolves.toBe(false);
});
it('throw an internal error when calling handle with unsupported input.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerCanNotHandle, handlerCanNotHandle ]);
await expect(handler.handle(null)).rejects.toThrow(InternalServerError);
});
it('returns the same handle results with handleSafe.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerFalse, handlerTrue, handlerCanNotHandle ]);
await expect(handler.handleSafe(null)).resolves.toBe(true);
});
it('throws the canHandle error when calling handleSafe with unsupported input.', async(): Promise<void> => {
const handler = new BooleanHandler([ handlerCanNotHandle, handlerCanNotHandle ]);
await expect(handler.handleSafe(null)).rejects.toThrow('Not supported, Not supported');
});
});

View File

@@ -1,84 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import {
CachedHandler,
} from '../../../../src/util/handlers/CachedHandler';
describe('A CachedHandler', (): void => {
const input: any = { field1: { key: 'value' }, field2: { key: 'value' }, field3: { key: 'value2' }};
const output = 'response';
let source: jest.Mocked<AsyncHandler<any, string>>;
let handler: CachedHandler<any, string>;
beforeEach(async(): Promise<void> => {
source = {
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(output),
} as any;
handler = new CachedHandler(source);
});
it('can handle input if its source can.', async(): Promise<void> => {
await expect(handler.canHandle(input)).resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenCalledTimes(1);
source.canHandle.mockRejectedValue(new Error('bad input'));
await expect(handler.canHandle(input)).rejects.toThrow('bad input');
expect(source.canHandle).toHaveBeenCalledTimes(2);
});
it('returns the source result.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toBe(output);
expect(source.handle).toHaveBeenCalledTimes(1);
expect(source.handle).toHaveBeenLastCalledWith(input);
});
it('caches the result.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toBe(output);
await expect(handler.handle(input)).resolves.toBe(output);
expect(source.handle).toHaveBeenCalledTimes(1);
});
it('caches on the object itself.', async(): Promise<void> => {
const copy = { ...input };
await expect(handler.handle(input)).resolves.toBe(output);
await expect(handler.handle(copy)).resolves.toBe(output);
expect(source.handle).toHaveBeenCalledTimes(2);
});
it('can handle the input if it has a cached result.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toBe(output);
source.canHandle.mockRejectedValue(new Error('bad input'));
await expect(handler.canHandle(input)).resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenCalledTimes(0);
});
it('cannot handle input with multiple keys if the first key is already missing.', async(): Promise<void> => {
handler = new CachedHandler(source, [ 'field1', 'field3' ]);
await expect(handler.canHandle(input)).resolves.toBeUndefined();
expect(source.canHandle).toHaveBeenCalledTimes(1);
});
it('can use a specific field of the input as key.', async(): Promise<void> => {
handler = new CachedHandler(source, [ 'field1' ]);
const copy = { ...input };
await expect(handler.handle(input)).resolves.toBe(output);
await expect(handler.handle(copy)).resolves.toBe(output);
expect(source.handle).toHaveBeenCalledTimes(1);
});
it('can use multiple fields of the object as keys.', async(): Promise<void> => {
handler = new CachedHandler(source, [ 'field1', 'field3' ]);
const copy = { ...input };
copy.field2 = { other: 'field' };
await expect(handler.handle(input)).resolves.toBe(output);
await expect(handler.handle(copy)).resolves.toBe(output);
expect(source.handle).toHaveBeenCalledTimes(1);
});
it('rejects empty field arrays.', async(): Promise<void> => {
expect((): any => new CachedHandler(source, []))
.toThrow('The fields parameter needs to have at least 1 entry if defined.');
});
});

View File

@@ -1,6 +1,6 @@
import type { AsyncHandler } from 'asynchronous-handlers';
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 => {

View File

@@ -1,48 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { filterHandlers, findHandler } from '../../../../src/util/handlers/HandlerUtil';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('HandlerUtil', (): void => {
describe('findHandler', (): void => {
let handlerTrue: AsyncHandler<any, any>;
let handlerFalse: AsyncHandler<any, any>;
beforeEach(async(): Promise<void> => {
handlerTrue = new StaticAsyncHandler(true, null);
handlerFalse = new StaticAsyncHandler(false, null);
});
it('finds a matching handler.', async(): Promise<void> => {
await expect(findHandler([ handlerFalse, handlerTrue ], null)).resolves.toBe(handlerTrue);
});
it('errors if there is no matching handler.', async(): Promise<void> => {
await expect(findHandler([ handlerFalse, handlerFalse ], null)).rejects.toThrow('Not supported, Not supported');
});
it('supports non-native Errors.', async(): Promise<void> => {
jest.spyOn(handlerFalse, 'canHandle').mockRejectedValue('apple');
await expect(findHandler([ handlerFalse ], null)).rejects.toThrow('Unknown error: apple');
});
});
describe('filterHandlers', (): void => {
let handlerTrue: AsyncHandler<any, any>;
let handlerFalse: AsyncHandler<any, any>;
beforeEach(async(): Promise<void> => {
handlerTrue = new StaticAsyncHandler(true, null);
handlerFalse = new StaticAsyncHandler(false, null);
});
it('finds matching handlers.', async(): Promise<void> => {
await expect(filterHandlers([ handlerTrue, handlerFalse, handlerTrue ], null))
.resolves.toEqual([ handlerTrue, handlerTrue ]);
});
it('errors if there is no matching handler.', async(): Promise<void> => {
await expect(filterHandlers([ handlerFalse, handlerFalse ], null))
.rejects.toThrow('Not supported, Not supported');
});
});
});

View File

@@ -1,7 +1,7 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import {
MethodFilterHandler,
} from '../../../../src/util/handlers/MethodFilterHandler';

View File

@@ -1,100 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { ParallelHandler } from '../../../../src/util/handlers/ParallelHandler';
describe('A ParallelHandler', (): void => {
const handlers: jest.Mocked<AsyncHandler<string, string>>[] = [
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('0'),
},
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('1'),
},
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('2'),
},
] as any;
const composite: ParallelHandler<string, string> = new ParallelHandler<string, string>(handlers);
afterEach(jest.clearAllMocks);
describe('canHandle', (): void => {
it('succeeds if all handlers succeed.', async(): Promise<void> => {
await expect(composite.canHandle('abc')).resolves.toBeUndefined();
expect(handlers[0].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[1].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[2].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[0].canHandle).toHaveBeenCalledWith('abc');
expect(handlers[1].canHandle).toHaveBeenCalledWith('abc');
expect(handlers[2].canHandle).toHaveBeenCalledWith('abc');
});
it('fails if one handler fails.', async(): Promise<void> => {
const error = new Error('failure');
handlers[1].canHandle.mockRejectedValueOnce(error);
await expect(composite.canHandle('abc')).rejects.toThrow(error);
});
});
describe('handle', (): void => {
it('succeeds if all handlers succeed.', async(): Promise<void> => {
await expect(composite.handle('abc')).resolves.toEqual([ '0', '1', '2' ]);
expect(handlers[0].handle).toHaveBeenCalledTimes(1);
expect(handlers[1].handle).toHaveBeenCalledTimes(1);
expect(handlers[2].handle).toHaveBeenCalledTimes(1);
expect(handlers[0].handle).toHaveBeenCalledWith('abc');
expect(handlers[1].handle).toHaveBeenCalledWith('abc');
expect(handlers[2].handle).toHaveBeenCalledWith('abc');
});
it('fails if one handler fails.', async(): Promise<void> => {
const error = new Error('failure');
handlers[1].handle.mockRejectedValueOnce(error);
await expect(composite.handle('abc')).rejects.toThrow(error);
});
});
describe('handleSafe', (): void => {
it('succeeds if all handlers succeed.', async(): Promise<void> => {
await expect(composite.handleSafe('abc')).resolves.toEqual([ '0', '1', '2' ]);
expect(handlers[0].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[1].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[2].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[0].canHandle).toHaveBeenCalledWith('abc');
expect(handlers[1].canHandle).toHaveBeenCalledWith('abc');
expect(handlers[2].canHandle).toHaveBeenCalledWith('abc');
expect(handlers[0].handle).toHaveBeenCalledTimes(1);
expect(handlers[1].handle).toHaveBeenCalledTimes(1);
expect(handlers[2].handle).toHaveBeenCalledTimes(1);
expect(handlers[0].handle).toHaveBeenCalledWith('abc');
expect(handlers[1].handle).toHaveBeenCalledWith('abc');
expect(handlers[2].handle).toHaveBeenCalledWith('abc');
});
it('fails if one canHandle fails.', async(): Promise<void> => {
const error = new Error('failure');
handlers[1].canHandle.mockRejectedValueOnce(error);
await expect(composite.handleSafe('abc')).rejects.toThrow(error);
expect(handlers[0].handle).toHaveBeenCalledTimes(0);
expect(handlers[1].handle).toHaveBeenCalledTimes(0);
expect(handlers[2].handle).toHaveBeenCalledTimes(0);
});
it('fails if one handle fails.', async(): Promise<void> => {
const error = new Error('failure');
handlers[1].handle.mockRejectedValueOnce(error);
await expect(composite.handleSafe('abc')).rejects.toThrow(error);
});
});
});

View File

@@ -1,72 +0,0 @@
import type { AsyncHandler, ClusterManager } from '../../../../src';
import { NotImplementedHttpError, ProcessHandler } from '../../../../src';
function createClusterManager(workers: number, primary: boolean): jest.Mocked<ClusterManager> {
return {
isSingleThreaded: jest.fn().mockReturnValue(workers === 1),
isWorker: jest.fn().mockReturnValue(!primary),
isPrimary: jest.fn().mockReturnValue(primary),
} as any;
}
describe('A ProcessHandler', (): void => {
const source: jest.Mocked<AsyncHandler<string, string>> = {
canHandle: jest.fn(),
handleSafe: jest.fn().mockResolvedValue('handledSafely'),
handle: jest.fn().mockResolvedValue('handled'),
};
describe('allowing only worker processes', (): void => {
it('can create a ProcessHandler.', (): void => {
expect((): ProcessHandler<string, string> =>
new ProcessHandler(source, createClusterManager(1, true), false)).toBeDefined();
});
it('can delegate to its source when run singlethreaded from worker.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(1, false), false);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('can delegate to its source when run singlethreaded from primary.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(1, true), false);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('can delegate to its source when run multithreaded from worker.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(2, false), false);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('errors when run multithreaded from primary.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(2, true), false);
await expect(ph.handleSafe('test')).rejects.toThrow(NotImplementedHttpError);
});
});
describe('allowing only the primary process', (): void => {
it('can create a ProcessHandler.', (): void => {
expect((): ProcessHandler<string, string> =>
new ProcessHandler(source, createClusterManager(1, true), true)).toBeDefined();
});
it('can delegate to its source when run singlethreaded from worker.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(1, false), true);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('can delegate to its source when run singlethreaded from primary.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(1, true), true);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('can delegate to its source when run multithreaded from primary.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(2, true), true);
await expect(ph.handleSafe('test')).resolves.toBe('handled');
});
it('errors when run multithreaded from worker.', async(): Promise<void> => {
const ph = new ProcessHandler(source, createClusterManager(2, false), true);
await expect(ph.handleSafe('test')).rejects.toThrow(NotImplementedHttpError);
});
});
});

View File

@@ -1,64 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { SequenceHandler } from '../../../../src/util/handlers/SequenceHandler';
describe('A SequenceHandler', (): void => {
const handlers: jest.Mocked<AsyncHandler<string, string>>[] = [
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('0'),
} as any,
{
canHandle: jest.fn().mockRejectedValue(new Error('not supported')),
handle: jest.fn().mockRejectedValue(new Error('should not be called')),
} as any,
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('2'),
} as any,
];
let composite: SequenceHandler<string, string>;
beforeEach(async(): Promise<void> => {
composite = new SequenceHandler<string, string>(handlers);
});
it('can handle all input.', async(): Promise<void> => {
await expect(composite.canHandle('test')).resolves.toBeUndefined();
});
it('runs all supported handlers.', async(): Promise<void> => {
await composite.handleSafe('test');
expect(handlers[0].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[0].canHandle).toHaveBeenLastCalledWith('test');
expect(handlers[0].handle).toHaveBeenCalledTimes(1);
expect(handlers[0].handle).toHaveBeenLastCalledWith('test');
expect(handlers[1].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[1].canHandle).toHaveBeenLastCalledWith('test');
expect(handlers[1].handle).toHaveBeenCalledTimes(0);
expect(handlers[2].canHandle).toHaveBeenCalledTimes(1);
expect(handlers[2].canHandle).toHaveBeenLastCalledWith('test');
expect(handlers[2].handle).toHaveBeenCalledTimes(1);
expect(handlers[2].handle).toHaveBeenLastCalledWith('test');
});
it('returns the result of the last supported handler.', async(): Promise<void> => {
await expect(composite.handleSafe('test')).resolves.toBe('2');
handlers[2].canHandle.mockRejectedValueOnce(new Error('not supported'));
await expect(composite.handleSafe('test')).resolves.toBe('0');
});
it('returns undefined if no handler is supported.', async(): Promise<void> => {
handlers[0].canHandle.mockRejectedValueOnce(new Error('not supported'));
handlers[2].canHandle.mockRejectedValueOnce(new Error('not supported'));
await expect(composite.handleSafe('test')).resolves.toBeUndefined();
});
it('errors if a handler errors.', async(): Promise<void> => {
handlers[2].handle.mockRejectedValueOnce(new Error('failure'));
await expect(composite.handleSafe('test')).rejects.toThrow('failure');
});
});

View File

@@ -1,18 +0,0 @@
import { StaticHandler } from '../../../../src/util/handlers/StaticHandler';
describe('A StaticHandler', (): void => {
it('can handle everything.', async(): Promise<void> => {
const handler = new StaticHandler();
await expect(handler.canHandle(null)).resolves.toBeUndefined();
});
it('returns the stored value.', async(): Promise<void> => {
const handler = new StaticHandler('apple');
await expect(handler.handle()).resolves.toBe('apple');
});
it('returns undefined if there is no stored value.', async(): Promise<void> => {
const handler = new StaticHandler();
await expect(handler.handle()).resolves.toBeUndefined();
});
});

View File

@@ -1,35 +0,0 @@
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { StaticThrowHandler } from '../../../../src/util/handlers/StaticThrowHandler';
import { SOLID_ERROR } from '../../../../src/util/Vocabularies';
describe('A StaticThrowHandler', (): void => {
const error = new BadRequestHttpError();
const handler = new StaticThrowHandler(error);
it('can handle all requests.', async(): Promise<void> => {
await expect(handler.canHandle({})).resolves.toBeUndefined();
});
it('always throws an instance of the given error.', async(): Promise<void> => {
await expect(handler.handle()).rejects.toThrow(error);
});
it('creates a new instance every time.', async(): Promise<void> => {
/* eslint-disable jest/no-conditional-expect */
try {
await handler.handle();
} catch (error: unknown) {
expect(BadRequestHttpError.isInstance(error)).toBe(true);
// Change the metadata
(error as BadRequestHttpError).metadata.add(SOLID_ERROR.terms.target, 'http://example.com/foo');
}
try {
await handler.handle();
} catch (error: unknown) {
expect(BadRequestHttpError.isInstance(error)).toBe(true);
// Metadata should not have the change
expect((error as BadRequestHttpError).metadata.has(SOLID_ERROR.terms.target)).toBe(false);
}
/* eslint-enable jest/no-conditional-expect */
});
});

View File

@@ -1,23 +1,23 @@
import { ArrayUnionHandler } from '../../../../src/util/handlers/ArrayUnionHandler';
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import type { AsyncHandler } from 'asynchronous-handlers';
import { StatusArrayUnionHandler } from '../../../../src/util/handlers/StatusArrayUnionHandler';
describe('An ArrayUnionHandler', (): void => {
describe('A StatusArrayUnionHandler', (): void => {
let handlers: jest.Mocked<AsyncHandler<string, number[]>>[];
let handler: ArrayUnionHandler<AsyncHandler<string, number[]>>;
let handler: StatusArrayUnionHandler<AsyncHandler<string, number[]>>;
beforeEach(async(): Promise<void> => {
handlers = [
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue([ 1, 2 ]),
} as any,
} satisfies Partial<AsyncHandler<string, number[]>> as any,
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue([ 3, 4 ]),
} as any,
} satisfies Partial<AsyncHandler<string, number[]>> as any,
];
handler = new ArrayUnionHandler(handlers);
handler = new StatusArrayUnionHandler(handlers);
});
it('merges the array results.', async(): Promise<void> => {

View File

@@ -0,0 +1,26 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import { StatusBooleanHandler } from '../../../../src/util/handlers/StatusBooleanHandler';
describe('A StatusBooleanHandler', (): void => {
let handlers: jest.Mocked<AsyncHandler<string, boolean>>[];
let handler: StatusBooleanHandler<string>;
beforeEach(async(): Promise<void> => {
handlers = [
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(false),
} satisfies Partial<AsyncHandler<string, boolean>> as any,
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(true),
} satisfies Partial<AsyncHandler<string, boolean>> as any,
];
handler = new StatusBooleanHandler(handlers);
});
it('returns true if one of the handlers returns true.', async(): Promise<void> => {
await expect(handler.handleSafe('input')).resolves.toBe(true);
});
});

View File

@@ -0,0 +1,50 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
import { ForbiddenHttpError } from '../../../../src/util/errors/ForbiddenHttpError';
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
import { MethodNotAllowedHttpError } from '../../../../src/util/errors/MethodNotAllowedHttpError';
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { StatusHandler } from '../../../../src/util/handlers/StatusHandler';
import { getError } from '../../../util/Util';
describe('A StatusHandler', (): void => {
it('converts non-HttpErrors to an HttpError.', async(): Promise<void> => {
const handler: AsyncHandler<string, void> = {
canHandle: jest.fn().mockRejectedValue(new Error('canHandle')),
handle: jest.fn().mockRejectedValue(new Error('handle')),
handleSafe: jest.fn().mockRejectedValue(new Error('handleSafe')),
};
const statusHandler = new StatusHandler(handler);
const canHandleError = await getError(async(): Promise<void> => statusHandler.canHandle('input'));
expect(InternalServerError.isInstance(canHandleError)).toBe(true);
expect(canHandleError.message).toBe('canHandle');
const handleError = await getError(async(): Promise<void> => statusHandler.handle('input'));
expect(InternalServerError.isInstance(handleError)).toBe(true);
expect(handleError.message).toBe('handle');
const handleSafeError = await getError(async(): Promise<void> => statusHandler.handleSafe('input'));
expect(InternalServerError.isInstance(handleSafeError)).toBe(true);
expect(handleSafeError.message).toBe('handleSafe');
});
it('converts AggregateErrors to HttpErrors.', async(): Promise<void> => {
const handler: AsyncHandler<string, void> = {
canHandle: jest.fn().mockRejectedValue(new MethodNotAllowedHttpError()),
handle: jest.fn().mockRejectedValue(new AggregateError([
new MethodNotAllowedHttpError(),
new ForbiddenHttpError(),
], 'error')),
handleSafe: jest.fn().mockRejectedValue(new AggregateError([
new MethodNotAllowedHttpError(),
new NotImplementedHttpError(),
], 'error')),
};
const statusHandler = new StatusHandler(handler);
await expect(statusHandler.canHandle('input')).rejects.toThrow(MethodNotAllowedHttpError);
await expect(statusHandler.handle('input')).rejects.toThrow(BadRequestHttpError);
await expect(statusHandler.handleSafe('input')).rejects.toThrow(InternalServerError);
});
});

View File

@@ -0,0 +1,43 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import { StatusUnionHandler } from '../../../../src/util/handlers/StatusUnionHandler';
class SimpleUnionHandler extends StatusUnionHandler<AsyncHandler<any, string>> {
protected async combine(results: string[]): Promise<string> {
return results.join('');
}
}
describe('A StatusUnionHandler', (): void => {
const input = { data: 'text' };
let handlers: jest.Mocked<AsyncHandler<any, string>>[];
let handler: SimpleUnionHandler;
beforeEach(async(): Promise<void> => {
handlers = [
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('a'),
} satisfies Partial<AsyncHandler<any, string>> as any,
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('b'),
} satisfies Partial<AsyncHandler<any, string>> as any,
];
handler = new SimpleUnionHandler(handlers, true);
});
it('calls the combine function when calling canHandle.', async(): Promise<void> => {
await expect(handler.canHandle(input)).resolves.toBeUndefined();
expect(handlers[0].canHandle).toHaveBeenLastCalledWith(input);
expect(handlers[1].canHandle).toHaveBeenLastCalledWith(input);
});
it('calls the combine function on handle calls.', async(): Promise<void> => {
await expect(handler.handle(input)).resolves.toBe('ab');
expect(handlers[0].handle).toHaveBeenLastCalledWith(input);
expect(handlers[1].handle).toHaveBeenLastCalledWith(input);
});
});

View File

@@ -0,0 +1,26 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import { StatusWaterfallHandler } from '../../../../src/util/handlers/StatusWaterfallHandler';
describe('A StatusBooleanHandler', (): void => {
let handlers: jest.Mocked<AsyncHandler<string, boolean>>[];
let handler: StatusWaterfallHandler<string, boolean>;
beforeEach(async(): Promise<void> => {
handlers = [
{
canHandle: jest.fn().mockRejectedValue(new Error('no handle')),
handle: jest.fn().mockResolvedValue(false),
} satisfies Partial<AsyncHandler<string, boolean>> as any,
{
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue(true),
} satisfies Partial<AsyncHandler<string, boolean>> as any,
];
handler = new StatusWaterfallHandler(handlers);
});
it('returns true if one of the handlers returns true.', async(): Promise<void> => {
await expect(handler.handleSafe('input')).resolves.toBe(true);
});
});

View File

@@ -1,81 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { UnionHandler } from '../../../../src/util/handlers/UnionHandler';
class SimpleUnionHandler extends UnionHandler<AsyncHandler<any, string>> {
protected async combine(results: string[]): Promise<string> {
return results.join('');
}
}
describe('A UnionHandler', (): void => {
const input = { data: 'text' };
let handlers: jest.Mocked<AsyncHandler<any, string>>[];
let handler: SimpleUnionHandler;
beforeEach(async(): Promise<void> => {
handlers = [
{ canHandle: jest.fn(), handle: jest.fn().mockResolvedValue('a') } as any,
{ canHandle: jest.fn(), handle: jest.fn().mockResolvedValue('b') } as any,
];
handler = new SimpleUnionHandler(handlers);
});
it('can handle a request if at least one extractor can handle it.', async(): Promise<void> => {
await expect(handler.canHandle(input)).resolves.toBeUndefined();
handlers[0].canHandle.mockRejectedValue(new Error('bad request'));
await expect(handler.canHandle(input)).resolves.toBeUndefined();
handlers[1].canHandle.mockRejectedValue(new Error('bad request'));
await expect(handler.canHandle(input)).rejects.toThrow('bad request');
await expect(handler.handleSafe(input)).rejects.toThrow('bad request');
});
it('requires all handlers to support the input if requireAll is true.', async(): Promise<void> => {
handler = new SimpleUnionHandler(handlers, true);
await expect(handler.canHandle(input)).resolves.toBeUndefined();
handlers[0].canHandle.mockRejectedValue(new Error('bad request'));
await expect(handler.canHandle(input)).rejects.toThrow('bad request');
await expect(handler.handleSafe(input)).rejects.toThrow('bad request');
});
it('calls all handlers that support the input.', async(): Promise<void> => {
handlers[0].canHandle.mockRejectedValue(new Error('bad request'));
await expect(handler.handle(input)).resolves.toBe('b');
await expect(handler.handleSafe(input)).resolves.toBe('b');
});
it('calls all handlers if requireAll is true.', async(): Promise<void> => {
handler = new SimpleUnionHandler(handlers, true);
await expect(handler.handleSafe(input)).resolves.toBe('ab');
// `handle` call does not need to check `canHandle` values anymore
handlers[0].canHandle.mockRejectedValue(new Error('bad request'));
await expect(handler.handle(input)).resolves.toBe('ab');
});
it('requires all handlers to succeed if requireAll is true.', async(): Promise<void> => {
handler = new SimpleUnionHandler(handlers, true);
handlers[0].handle.mockRejectedValue(new Error('bad request'));
await expect(handler.handleSafe(input)).rejects.toThrow('bad request');
});
it('does not require all handlers to succeed if ignoreErrors is true.', async(): Promise<void> => {
handler = new SimpleUnionHandler(handlers, true, true);
handlers[0].handle.mockRejectedValueOnce(new Error('bad request'));
await expect(handler.handleSafe(input)).resolves.toBe('b');
handlers[1].handle.mockRejectedValueOnce(new Error('bad request'));
await expect(handler.handleSafe(input)).resolves.toBe('a');
handlers[0].handle.mockRejectedValueOnce(new Error('bad request'));
handlers[1].handle.mockRejectedValueOnce(new Error('bad request'));
await expect(handler.handleSafe(input)).resolves.toBe('');
});
});

View File

@@ -1,18 +0,0 @@
import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError';
import { UnsupportedAsyncHandler } from '../../../../src/util/handlers/UnsupportedAsyncHandler';
describe('An UnsupportedAsyncHandler', (): void => {
it('throws a default error when no message is set.', async(): Promise<void> => {
const handler = new UnsupportedAsyncHandler();
await expect(handler.canHandle()).rejects.toThrow(NotImplementedHttpError);
await expect(handler.handle()).rejects.toThrow(NotImplementedHttpError);
await expect(handler.handleSafe(null)).rejects.toThrow(NotImplementedHttpError);
});
it('throws the specified error when a message is set.', async(): Promise<void> => {
const handler = new UnsupportedAsyncHandler('custom error');
await expect(handler.canHandle()).rejects.toThrow('custom error');
await expect(handler.handle()).rejects.toThrow('custom error');
await expect(handler.handleSafe(null)).rejects.toThrow('custom error');
});
});

View File

@@ -1,86 +0,0 @@
import type { AsyncHandler } from '../../../../src/util/handlers/AsyncHandler';
import { WaterfallHandler } from '../../../../src/util/handlers/WaterfallHandler';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A WaterfallHandler', (): void => {
describe('with no handlers', (): void => {
it('can never handle data.', async(): Promise<void> => {
const handler = new WaterfallHandler([]);
await expect(handler.canHandle(null)).rejects.toThrow(Error);
});
it('errors if its handle function is called.', async(): Promise<void> => {
const handler = new WaterfallHandler([]);
await expect(handler.handle(null)).rejects.toThrow(Error);
});
});
describe('with multiple handlers', (): void => {
let handlerTrue: AsyncHandler<any, any>;
let handlerFalse: AsyncHandler<any, any>;
let canHandleFn: jest.Mock<Promise<void>, [any]>;
let handleFn: jest.Mock<Promise<void>, [any]>;
beforeEach(async(): Promise<void> => {
handlerTrue = new StaticAsyncHandler(true, null);
handlerFalse = new StaticAsyncHandler(false, null);
canHandleFn = jest.fn(async(input: any): Promise<any> => input);
handleFn = jest.fn(async(input: any): Promise<any> => input);
handlerTrue.canHandle = canHandleFn;
handlerTrue.handle = handleFn;
});
it('can handle data if a handler supports it.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerTrue ]);
await expect(handler.canHandle(null)).resolves.toBeUndefined();
});
it('can not handle data if no handler supports it.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerFalse ]);
await expect(handler.canHandle(null)).rejects.toThrow('Not supported, Not supported');
});
it('throws unknown errors if no Error objects are thrown.', async(): Promise<void> => {
handlerFalse.canHandle = async(): Promise<void> => {
// eslint-disable-next-line ts/no-throw-literal
throw 'apple';
};
const handler = new WaterfallHandler([ handlerFalse, handlerFalse ]);
await expect(handler.canHandle(null)).rejects.toThrow('Unknown error: apple, Unknown error: apple');
});
it('handles data if a handler supports it.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerTrue ]);
await expect(handler.handle('test')).resolves.toBe('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});
it('errors if the handle function is called but no handler supports the data.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerFalse ]);
await expect(handler.handle('test')).rejects.toThrow('All handlers failed');
});
it('only calls the canHandle function once of its handlers when handleSafe is called.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerTrue ]);
await expect(handler.handleSafe('test')).resolves.toBe('test');
expect(canHandleFn).toHaveBeenCalledTimes(1);
expect(handleFn).toHaveBeenCalledTimes(1);
});
it('throws the canHandle error when calling handleSafe if the data is not supported.', async(): Promise<void> => {
const handler = new WaterfallHandler([ handlerFalse, handlerFalse ]);
await expect(handler.handleSafe(null)).rejects.toThrow('Not supported, Not supported');
});
});
});

View File

@@ -1,5 +1,6 @@
import type { AsyncHandler } from 'asynchronous-handlers';
import { NotFoundHttpError, StaticTemplateEngine } from '../../../../src';
import type { AsyncHandler, TemplateEngineInput } from '../../../../src';
import type { TemplateEngineInput } from '../../../../src';
import Dict = NodeJS.Dict;
describe('A StaticTemplateEngine', (): void => {