feat: Create an OperationRouterHandler

This allows us to route after an Operation has been parsed
This commit is contained in:
Joachim Van Herwegen
2022-09-29 16:46:46 +02:00
parent da99ff30f6
commit 3db1921633
14 changed files with 266 additions and 154 deletions

View File

@@ -0,0 +1,84 @@
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 }>>) {
super(args);
}
public async canHandle(input: { method: string; target: ResourceIdentifier }): Promise<void> {
await this.canHandleInput(input, input.method, input.target);
}
}
describe('A BaseRouterHandler', (): void => {
const baseUrl = 'http://example.com/';
const method = 'GET';
const target: ResourceIdentifier = { path: 'http://example.com/foo' };
let handler: jest.Mocked<AsyncHandler<{ method: string; target: ResourceIdentifier }>>;
let router: SimpleRouterHandler;
beforeEach(async(): Promise<void> => {
handler = {
canHandle: jest.fn(),
handle: jest.fn().mockResolvedValue('result'),
} as any;
router = new SimpleRouterHandler({
baseUrl,
handler,
allowedPathNames: [ '^/foo$', '^/bar$' ],
allowedMethods: [ 'GET', 'HEAD' ],
});
});
it('requires the correct method.', async(): Promise<void> => {
await expect(router.canHandle({ method: 'POST', target })).rejects.toThrow('POST is not allowed');
});
it('requires the path to match a given regex.', async(): Promise<void> => {
await expect(router.canHandle({ method, target: { path: 'http://example.com/baz' }}))
.rejects.toThrow('Cannot handle route /baz');
});
it('accepts valid input.', async(): Promise<void> => {
await expect(router.canHandle({ method, target })).resolves.toBeUndefined();
});
it('requires the source handler to accept the input.', async(): Promise<void> => {
handler.canHandle.mockRejectedValue(new Error('bad input'));
await expect(router.canHandle({ method, target })).rejects.toThrow('bad input');
});
it('accepts all methods if no restrictions are defined.', async(): Promise<void> => {
router = new SimpleRouterHandler({
baseUrl,
handler,
allowedPathNames: [ '^/foo$', '^/bar$' ],
});
await expect(router.canHandle({ method: 'POST', target })).resolves.toBeUndefined();
});
it('accepts all paths if no restrictions are defined.', async(): Promise<void> => {
router = new SimpleRouterHandler({
handler,
allowedMethods: [ 'GET', 'HEAD' ],
});
await expect(router.canHandle({ method, target: { path: 'http://example.com/baz' }})).resolves.toBeUndefined();
});
it('requires a baseUrl input if there is a path restriction.', async(): Promise<void> => {
expect((): any => new SimpleRouterHandler({
handler,
allowedPathNames: [ '^/foo$', '^/bar$' ],
})).toThrow('A value for allowedPathNames requires baseUrl to be defined.');
});
it('calls the source handler.', async(): Promise<void> => {
await expect(router.handle({ method, target })).resolves.toBe('result');
expect(handler.handle).toHaveBeenCalledTimes(1);
expect(handler.handle).toHaveBeenLastCalledWith({ method, target });
});
});

View File

@@ -0,0 +1,44 @@
import type { Operation } from '../../../../src/http/Operation';
import { BasicRepresentation } from '../../../../src/http/representation/BasicRepresentation';
import type { HttpRequest } from '../../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../../src/server/HttpResponse';
import type { OperationHttpHandler } from '../../../../src/server/OperationHttpHandler';
import { OperationRouterHandler } from '../../../../src/server/util/OperationRouterHandler';
describe('An OperationRouterHandler', (): void => {
const request: HttpRequest = {} as any;
const response: HttpResponse = {} as any;
let operation: Operation;
let handler: jest.Mocked<OperationHttpHandler>;
let router: OperationRouterHandler;
beforeEach(async(): Promise<void> => {
operation = {
method: 'GET',
target: { path: 'http://example.com/foo' },
preferences: {},
body: new BasicRepresentation(),
};
handler = {
canHandle: jest.fn(),
handle: jest.fn(),
} as any;
router = new OperationRouterHandler({
baseUrl: 'http://example.com/',
handler,
allowedPathNames: [ '^/foo$' ],
allowedMethods: [ 'GET' ],
});
});
it('passes the operation values.', async(): Promise<void> => {
await expect(router.canHandle({ operation, request, response })).resolves.toBeUndefined();
operation.method = 'POST';
await expect(router.canHandle({ operation, request, response })).rejects.toThrow('POST is not allowed.');
operation.method = 'GET';
operation.target = { path: 'http://example.com/wrong' };
await expect(router.canHandle({ operation, request, response })).rejects.toThrow('Cannot handle route /wrong');
});
});

View File

@@ -1,131 +1,57 @@
import { createRequest, createResponse } from 'node-mocks-http';
import type {
AsyncHandler,
HttpHandlerInput,
HttpRequest,
import type { HttpRequest,
HttpResponse,
TargetExtractor,
ResourceIdentifier,
RouterHandlerArgs,
} from '../../../../src';
import { guardStream, joinUrl } from '../../../../src';
HttpHandler } from '../../../../src';
import { joinUrl } from '../../../../src';
import { RouterHandler } from '../../../../src/server/util/RouterHandler';
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
describe('A RouterHandler', (): void => {
const baseUrl = 'http://test.com/foo/';
const baseUrl = 'http://test.com/';
let targetExtractor: jest.Mocked<TargetExtractor>;
let subHandler: AsyncHandler<any, any>;
let genericRequest: HttpRequest;
let genericResponse: HttpResponse;
let genericInput: HttpHandlerInput;
let args: RouterHandlerArgs;
let request: HttpRequest;
const response: HttpResponse = {} as any;
let handler: jest.Mocked<HttpHandler>;
let router: RouterHandler;
beforeEach((): void => {
request = { method: 'GET', url: '/test' } as any;
targetExtractor = {
handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })),
} as any;
subHandler = new StaticAsyncHandler(true, undefined);
handler = {
canHandle: jest.fn(),
handle: jest.fn(),
} as any;
args = {
router = new RouterHandler({
baseUrl,
targetExtractor,
handler: subHandler,
allowedMethods: [],
allowedPathNames: [],
};
genericRequest = guardStream(createRequest({
url: '/test',
}));
genericResponse = createResponse() as HttpResponse;
genericInput = {
request: genericRequest,
response: genericResponse,
};
handler,
allowedMethods: [ 'GET' ],
allowedPathNames: [ '^/test$' ],
});
});
it('calls the sub handler when handle is called.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
expect(await handler.handle(genericInput)).toBeUndefined();
it('errors if there is no url.', async(): Promise<void> => {
delete request.url;
await expect(router.canHandle({ request, response }))
.rejects.toThrow('Cannot handle request without a url');
});
it('throws an error if the request does not have a url.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
const request = guardStream(createRequest());
await expect(handler.canHandle({
request,
response: genericResponse,
})).rejects.toThrow('Cannot handle request without a url');
it('passes the request method.', async(): Promise<void> => {
await expect(router.canHandle({ request, response })).resolves.toBeUndefined();
request.method = 'POST';
await expect(router.canHandle({ request, response })).rejects.toThrow('POST is not allowed.');
delete request.method;
await expect(router.canHandle({ request, response })).rejects.toThrow('UNKNOWN is not allowed.');
});
it('throws an error if the request does not have a method.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
const request = guardStream(createRequest({
url: '/test',
}));
// @ts-expect-error manually set the method
request.method = undefined;
await expect(handler.canHandle({
request,
response: genericResponse,
})).rejects.toThrow('Cannot handle request without a method');
});
it('throws an error when there are no allowed methods or pathnames.', async(): Promise<void> => {
args.allowedMethods = [];
args.allowedPathNames = [];
const handler = new RouterHandler(args);
await expect(handler.canHandle(genericInput)).rejects.toThrow('GET is not allowed.');
});
it('throws an error when there are no allowed methods.', async(): Promise<void> => {
args.allowedMethods = [];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
await expect(handler.canHandle(genericInput)).rejects.toThrow('GET is not allowed.');
});
it('throws an error when there are no allowed pathnames.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [];
const handler = new RouterHandler(args);
await expect(handler.canHandle(genericInput)).rejects.toThrow('Cannot handle route /test');
});
it('throws an error if the RegEx string is not valid Regex.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '[' ];
expect((): RouterHandler => new RouterHandler(args))
.toThrow('Invalid regular expression: /[/: Unterminated character class');
});
it('throws an error if all else is successful, but the sub handler cannot handle.', async(): Promise<void> => {
args.handler = new StaticAsyncHandler(false, undefined);
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
await expect(handler.canHandle(genericInput)).rejects.toThrow('Not supported');
});
it('does not throw an error if the sub handler is successful.', async(): Promise<void> => {
args.allowedMethods = [ 'GET' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
expect(await handler.canHandle(genericInput)).toBeUndefined();
});
it('supports * for all methods.', async(): Promise<void> => {
args.allowedMethods = [ '*' ];
args.allowedPathNames = [ '/test' ];
const handler = new RouterHandler(args);
expect(await handler.canHandle(genericInput)).toBeUndefined();
it('generates a ResourceIdentifier based on the url.', async(): Promise<void> => {
await expect(router.canHandle({ request, response })).resolves.toBeUndefined();
request.url = '/wrongTest';
await expect(router.canHandle({ request, response })).rejects.toThrow('Cannot handle route /wrongTest');
});
});