mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create an OperationRouterHandler
This allows us to route after an Operation has been parsed
This commit is contained in:
parent
da99ff30f6
commit
3db1921633
@ -3,6 +3,7 @@
|
|||||||
"Adapter",
|
"Adapter",
|
||||||
"BaseActivityEmitter",
|
"BaseActivityEmitter",
|
||||||
"BaseHttpError",
|
"BaseHttpError",
|
||||||
|
"BaseRouterHandler",
|
||||||
"BasicConditions",
|
"BasicConditions",
|
||||||
"BasicRepresentation",
|
"BasicRepresentation",
|
||||||
"ChangeMap",
|
"ChangeMap",
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
"@type": "RouterHandler",
|
"@type": "RouterHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
|
||||||
"args_allowedPathNames": [ "/setup" ],
|
"args_allowedPathNames": [ "/setup" ],
|
||||||
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
|
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
"@type": "RouterHandler",
|
"@type": "RouterHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
|
||||||
"args_allowedPathNames": [ "/setup" ],
|
"args_allowedPathNames": [ "/setup" ],
|
||||||
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
|
"args_handler": { "@id": "urn:solid-server:default:SetupParsingHandler" }
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
"@type": "RouterHandler",
|
"@type": "RouterHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
|
||||||
"args_allowedPathNames": [ "^/.oidc/.*", "^/\\.well-known/openid-configuration" ],
|
"args_allowedPathNames": [ "^/.oidc/.*", "^/\\.well-known/openid-configuration" ],
|
||||||
"args_handler": {
|
"args_handler": {
|
||||||
"@type": "OidcHttpHandler",
|
"@type": "OidcHttpHandler",
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
"@type": "RouterHandler",
|
"@type": "RouterHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
|
||||||
"args_allowedPathNames": [ "^/idp/.*" ],
|
"args_allowedPathNames": [ "^/idp/.*" ],
|
||||||
"args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
|
"args_handler": { "@id": "urn:solid-server:default:IdentityProviderParsingHandler" }
|
||||||
},
|
},
|
||||||
|
@ -60,7 +60,6 @@
|
|||||||
"@type": "RouterHandler",
|
"@type": "RouterHandler",
|
||||||
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
"args_baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" },
|
||||||
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
"args_targetExtractor": { "@id": "urn:solid-server:default:TargetExtractor" },
|
||||||
"args_allowedMethods": [ "*" ],
|
|
||||||
"args_allowedPathNames": [ "^/.*\\.acl$" ],
|
"args_allowedPathNames": [ "^/.*\\.acl$" ],
|
||||||
"args_handler": { "@id": "urn:solid-server:default:LdpHandler" }
|
"args_handler": { "@id": "urn:solid-server:default:LdpHandler" }
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export interface OriginalUrlExtractorArgs {
|
|||||||
identifierStrategy: IdentifierStrategy;
|
identifierStrategy: IdentifierStrategy;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify wether the OriginalUrlExtractor should include the request query string.
|
* Specify whether the OriginalUrlExtractor should include the request query string.
|
||||||
*/
|
*/
|
||||||
includeQueryString?: boolean;
|
includeQueryString?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -305,6 +305,8 @@ export * from './server/middleware/WebSocketAdvertiser';
|
|||||||
export * from './server/notifications/ActivityEmitter';
|
export * from './server/notifications/ActivityEmitter';
|
||||||
|
|
||||||
// Server/Util
|
// Server/Util
|
||||||
|
export * from './server/util/BaseRouterHandler';
|
||||||
|
export * from './server/util/OperationRouterHandler';
|
||||||
export * from './server/util/RedirectingHttpHandler';
|
export * from './server/util/RedirectingHttpHandler';
|
||||||
export * from './server/util/RouterHandler';
|
export * from './server/util/RouterHandler';
|
||||||
|
|
||||||
|
75
src/server/util/BaseRouterHandler.ts
Normal file
75
src/server/util/BaseRouterHandler.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||||
|
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
|
||||||
|
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
||||||
|
import type { AsyncHandlerInput, AsyncHandlerOutput } from '../../util/handlers/AsyncHandler';
|
||||||
|
import { AsyncHandler } from '../../util/handlers/AsyncHandler';
|
||||||
|
import { trimTrailingSlashes } from '../../util/PathUtil';
|
||||||
|
|
||||||
|
export interface BaseRouterHandlerArgs<T extends AsyncHandler<any, any>> {
|
||||||
|
/**
|
||||||
|
* The base URL of the server.
|
||||||
|
* Not required if no value is provided for `allowedPathNames`.
|
||||||
|
*/
|
||||||
|
baseUrl?: string;
|
||||||
|
/**
|
||||||
|
* The handler to call if all checks pass.
|
||||||
|
*/
|
||||||
|
handler: T;
|
||||||
|
/**
|
||||||
|
* The allowed method(s). `*` can be used to indicate all methods are allowed.
|
||||||
|
* Default is `[ '*' ]`.
|
||||||
|
*/
|
||||||
|
allowedMethods?: string[];
|
||||||
|
/**
|
||||||
|
* Regular expression(s) used to match the target URL.
|
||||||
|
* The base URl without trailing slash will be stripped of before applying the regular expressions,
|
||||||
|
* so the input will always start with a `/`.
|
||||||
|
* Default is `[ '.*' ]`.
|
||||||
|
*/
|
||||||
|
allowedPathNames?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given method and path are satisfied and allows its handler to be executed if so.
|
||||||
|
*
|
||||||
|
* Implementations of this class should call `canHandleInput` in their `canHandle` call with the correct parameters.
|
||||||
|
*
|
||||||
|
* `canHandleInput` expects a ResourceIdentifier to indicate it expects the target to have been validated already.
|
||||||
|
*/
|
||||||
|
export abstract class BaseRouterHandler<T extends AsyncHandler<any, any>>
|
||||||
|
extends AsyncHandler<AsyncHandlerInput<T>, AsyncHandlerOutput<T>> {
|
||||||
|
protected readonly baseUrlLength: number;
|
||||||
|
protected readonly handler: T;
|
||||||
|
protected readonly allowedMethods: string[];
|
||||||
|
protected readonly allMethods: boolean;
|
||||||
|
protected readonly allowedPathNamesRegEx: RegExp[];
|
||||||
|
|
||||||
|
protected constructor(args: BaseRouterHandlerArgs<T>) {
|
||||||
|
super();
|
||||||
|
if (typeof args.allowedPathNames !== 'undefined' && typeof args.baseUrl !== 'string') {
|
||||||
|
throw new Error('A value for allowedPathNames requires baseUrl to be defined.');
|
||||||
|
}
|
||||||
|
// Trimming trailing slash so regexes can start with `/`
|
||||||
|
this.baseUrlLength = trimTrailingSlashes(args.baseUrl ?? '').length;
|
||||||
|
this.handler = args.handler;
|
||||||
|
this.allowedMethods = args.allowedMethods ?? [ '*' ];
|
||||||
|
this.allMethods = this.allowedMethods.includes('*');
|
||||||
|
this.allowedPathNamesRegEx = (args.allowedPathNames ?? [ '.*' ]).map((pn): RegExp => new RegExp(pn, 'u'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async canHandleInput(input: AsyncHandlerInput<T>, method: string, target: ResourceIdentifier):
|
||||||
|
Promise<void> {
|
||||||
|
if (!this.allMethods && !this.allowedMethods.includes(method)) {
|
||||||
|
throw new MethodNotAllowedHttpError([ method ], `${method} is not allowed.`);
|
||||||
|
}
|
||||||
|
const pathName = target.path.slice(this.baseUrlLength);
|
||||||
|
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {
|
||||||
|
throw new NotFoundHttpError(`Cannot handle route ${pathName}`);
|
||||||
|
}
|
||||||
|
await this.handler.canHandle(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handle(input: AsyncHandlerInput<T>): Promise<AsyncHandlerOutput<T>> {
|
||||||
|
return this.handler.handle(input);
|
||||||
|
}
|
||||||
|
}
|
16
src/server/util/OperationRouterHandler.ts
Normal file
16
src/server/util/OperationRouterHandler.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { OperationHttpHandlerInput, OperationHttpHandler } from '../OperationHttpHandler';
|
||||||
|
import type { BaseRouterHandlerArgs } from './BaseRouterHandler';
|
||||||
|
import { BaseRouterHandler } from './BaseRouterHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link BaseRouterHandler} for an {@link OperationHttpHandler}.
|
||||||
|
*/
|
||||||
|
export class OperationRouterHandler extends BaseRouterHandler<OperationHttpHandler> {
|
||||||
|
public constructor(args: BaseRouterHandlerArgs<OperationHttpHandler>) {
|
||||||
|
super(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async canHandle(input: OperationHttpHandlerInput): Promise<void> {
|
||||||
|
await super.canHandleInput(input, input.operation.method, input.operation.target);
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,23 @@
|
|||||||
import type { TargetExtractor } from '../../http/input/identifier/TargetExtractor';
|
import type { TargetExtractor } from '../../http/input/identifier/TargetExtractor';
|
||||||
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
import { BadRequestHttpError } from '../../util/errors/BadRequestHttpError';
|
||||||
import { MethodNotAllowedHttpError } from '../../util/errors/MethodNotAllowedHttpError';
|
import type { HttpHandlerInput, HttpHandler } from '../HttpHandler';
|
||||||
import { NotFoundHttpError } from '../../util/errors/NotFoundHttpError';
|
import { BaseRouterHandler } from './BaseRouterHandler';
|
||||||
import { ensureTrailingSlash, getRelativeUrl } from '../../util/PathUtil';
|
import type { BaseRouterHandlerArgs } from './BaseRouterHandler';
|
||||||
import type { HttpHandlerInput } from '../HttpHandler';
|
|
||||||
import { HttpHandler } from '../HttpHandler';
|
|
||||||
|
|
||||||
export interface RouterHandlerArgs {
|
export interface RouterHandlerArgs extends BaseRouterHandlerArgs<HttpHandler> {
|
||||||
baseUrl: string;
|
|
||||||
targetExtractor: TargetExtractor;
|
targetExtractor: TargetExtractor;
|
||||||
handler: HttpHandler;
|
|
||||||
allowedMethods: string[];
|
|
||||||
allowedPathNames: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An HttpHandler that checks if a given method and path are satisfied
|
* A {@link BaseRouterHandler} for an {@link HttpHandler}.
|
||||||
* and allows its handler to be executed if so.
|
* Uses a {@link TargetExtractor} to generate the target identifier.
|
||||||
*
|
|
||||||
* If `allowedMethods` contains '*' it will match all methods.
|
|
||||||
*/
|
*/
|
||||||
export class RouterHandler extends HttpHandler {
|
export class RouterHandler extends BaseRouterHandler<HttpHandler> {
|
||||||
private readonly baseUrl: string;
|
|
||||||
private readonly targetExtractor: TargetExtractor;
|
private readonly targetExtractor: TargetExtractor;
|
||||||
private readonly handler: HttpHandler;
|
|
||||||
private readonly allowedMethods: string[];
|
|
||||||
private readonly allMethods: boolean;
|
|
||||||
private readonly allowedPathNamesRegEx: RegExp[];
|
|
||||||
|
|
||||||
public constructor(args: RouterHandlerArgs) {
|
public constructor(args: RouterHandlerArgs) {
|
||||||
super();
|
super(args);
|
||||||
this.baseUrl = ensureTrailingSlash(args.baseUrl);
|
|
||||||
this.targetExtractor = args.targetExtractor;
|
this.targetExtractor = args.targetExtractor;
|
||||||
this.handler = args.handler;
|
|
||||||
this.allowedMethods = args.allowedMethods;
|
|
||||||
this.allMethods = args.allowedMethods.includes('*');
|
|
||||||
this.allowedPathNamesRegEx = args.allowedPathNames.map((pn): RegExp => new RegExp(pn, 'u'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async canHandle(input: HttpHandlerInput): Promise<void> {
|
public async canHandle(input: HttpHandlerInput): Promise<void> {
|
||||||
@ -43,20 +25,7 @@ export class RouterHandler extends HttpHandler {
|
|||||||
if (!request.url) {
|
if (!request.url) {
|
||||||
throw new BadRequestHttpError('Cannot handle request without a url');
|
throw new BadRequestHttpError('Cannot handle request without a url');
|
||||||
}
|
}
|
||||||
if (!request.method) {
|
const target = await this.targetExtractor.handleSafe({ request });
|
||||||
throw new BadRequestHttpError('Cannot handle request without a method');
|
await super.canHandleInput(input, request.method ?? 'UNKNOWN', target);
|
||||||
}
|
|
||||||
if (!this.allMethods && !this.allowedMethods.includes(request.method)) {
|
|
||||||
throw new MethodNotAllowedHttpError([ request.method ], `${request.method} is not allowed.`);
|
|
||||||
}
|
|
||||||
const pathName = await getRelativeUrl(this.baseUrl, request, this.targetExtractor);
|
|
||||||
if (!this.allowedPathNamesRegEx.some((regex): boolean => regex.test(pathName))) {
|
|
||||||
throw new NotFoundHttpError(`Cannot handle route ${pathName}`);
|
|
||||||
}
|
|
||||||
await this.handler.canHandle(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
|
||||||
await this.handler.handle(input);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
test/unit/server/util/BaseRouterHandler.test.ts
Normal file
84
test/unit/server/util/BaseRouterHandler.test.ts
Normal 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 });
|
||||||
|
});
|
||||||
|
});
|
44
test/unit/server/util/OperationRouterHandler.test.ts
Normal file
44
test/unit/server/util/OperationRouterHandler.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
@ -1,131 +1,57 @@
|
|||||||
import { createRequest, createResponse } from 'node-mocks-http';
|
import type { HttpRequest,
|
||||||
import type {
|
|
||||||
AsyncHandler,
|
|
||||||
HttpHandlerInput,
|
|
||||||
HttpRequest,
|
|
||||||
HttpResponse,
|
HttpResponse,
|
||||||
TargetExtractor,
|
TargetExtractor,
|
||||||
ResourceIdentifier,
|
ResourceIdentifier,
|
||||||
RouterHandlerArgs,
|
HttpHandler } from '../../../../src';
|
||||||
} from '../../../../src';
|
import { joinUrl } from '../../../../src';
|
||||||
import { guardStream, joinUrl } from '../../../../src';
|
|
||||||
import { RouterHandler } from '../../../../src/server/util/RouterHandler';
|
import { RouterHandler } from '../../../../src/server/util/RouterHandler';
|
||||||
import { StaticAsyncHandler } from '../../../util/StaticAsyncHandler';
|
|
||||||
|
|
||||||
describe('A RouterHandler', (): void => {
|
describe('A RouterHandler', (): void => {
|
||||||
const baseUrl = 'http://test.com/foo/';
|
const baseUrl = 'http://test.com/';
|
||||||
let targetExtractor: jest.Mocked<TargetExtractor>;
|
let targetExtractor: jest.Mocked<TargetExtractor>;
|
||||||
let subHandler: AsyncHandler<any, any>;
|
let request: HttpRequest;
|
||||||
let genericRequest: HttpRequest;
|
const response: HttpResponse = {} as any;
|
||||||
let genericResponse: HttpResponse;
|
let handler: jest.Mocked<HttpHandler>;
|
||||||
let genericInput: HttpHandlerInput;
|
let router: RouterHandler;
|
||||||
let args: RouterHandlerArgs;
|
|
||||||
|
|
||||||
beforeEach((): void => {
|
beforeEach((): void => {
|
||||||
|
request = { method: 'GET', url: '/test' } as any;
|
||||||
|
|
||||||
targetExtractor = {
|
targetExtractor = {
|
||||||
handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })),
|
handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })),
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
subHandler = new StaticAsyncHandler(true, undefined);
|
handler = {
|
||||||
|
canHandle: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
args = {
|
router = new RouterHandler({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
targetExtractor,
|
targetExtractor,
|
||||||
handler: subHandler,
|
handler,
|
||||||
allowedMethods: [],
|
allowedMethods: [ 'GET' ],
|
||||||
allowedPathNames: [],
|
allowedPathNames: [ '^/test$' ],
|
||||||
};
|
});
|
||||||
|
|
||||||
genericRequest = guardStream(createRequest({
|
|
||||||
url: '/test',
|
|
||||||
}));
|
|
||||||
genericResponse = createResponse() as HttpResponse;
|
|
||||||
genericInput = {
|
|
||||||
request: genericRequest,
|
|
||||||
response: genericResponse,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the sub handler when handle is called.', async(): Promise<void> => {
|
it('errors if there is no url.', async(): Promise<void> => {
|
||||||
args.allowedMethods = [ 'GET' ];
|
delete request.url;
|
||||||
args.allowedPathNames = [ '/test' ];
|
await expect(router.canHandle({ request, response }))
|
||||||
const handler = new RouterHandler(args);
|
.rejects.toThrow('Cannot handle request without a url');
|
||||||
expect(await handler.handle(genericInput)).toBeUndefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if the request does not have a url.', async(): Promise<void> => {
|
it('passes the request method.', async(): Promise<void> => {
|
||||||
args.allowedMethods = [ 'GET' ];
|
await expect(router.canHandle({ request, response })).resolves.toBeUndefined();
|
||||||
args.allowedPathNames = [ '/test' ];
|
request.method = 'POST';
|
||||||
const handler = new RouterHandler(args);
|
await expect(router.canHandle({ request, response })).rejects.toThrow('POST is not allowed.');
|
||||||
const request = guardStream(createRequest());
|
delete request.method;
|
||||||
await expect(handler.canHandle({
|
await expect(router.canHandle({ request, response })).rejects.toThrow('UNKNOWN is not allowed.');
|
||||||
request,
|
|
||||||
response: genericResponse,
|
|
||||||
})).rejects.toThrow('Cannot handle request without a url');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if the request does not have a method.', async(): Promise<void> => {
|
it('generates a ResourceIdentifier based on the url.', async(): Promise<void> => {
|
||||||
args.allowedMethods = [ 'GET' ];
|
await expect(router.canHandle({ request, response })).resolves.toBeUndefined();
|
||||||
args.allowedPathNames = [ '/test' ];
|
request.url = '/wrongTest';
|
||||||
const handler = new RouterHandler(args);
|
await expect(router.canHandle({ request, response })).rejects.toThrow('Cannot handle route /wrongTest');
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user