diff --git a/CHANGELOG.md b/CHANGELOG.md index 3629380..7d9767b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # Changelog + All notable changes to this project from 6.4.4 forward will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -12,17 +13,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed can't set headers after they are sent (#255 / #412) + ## [6.4.7] ### Fixed -- Updated `inversify` and `express` dependencies to be peer dependnecies as stated in the docs. + +- Updated `inversify` and `express` dependencies to be peer dependnecies as stated in the docs. ## [6.4.4] ### Added ### Changed -- Update dependencies (`minimist`, `json5`, `@babel/traverse`, `tough-cookie`, `ansi-regex`, `cookiejar`, `express`, `decode-uri-component`) + +- Update dependencies (`minimist`, `json5`, `@babel/traverse`, `tough-cookie`, `ansi-regex`, `cookiejar`, `express`, `decode-uri-component`) ### Fixed -- Change JsonContent to return object rather than string (#379 / #378) + +- Change JsonContent to return object rather than string (#379 / #378) diff --git a/src/server.ts b/src/server.ts index 7f3ee95..0f693fe 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,411 +1,395 @@ -import 'reflect-metadata'; +import "reflect-metadata"; + +import express, { + Application, + NextFunction, + Request, + RequestHandler, + Response, + Router, +} from "express"; +import { interfaces } from "inversify"; +import { BaseMiddleware, Controller } from "./index"; +import { + getControllersFromMetadata, + getControllersFromContainer, + getControllerMetadata, + getControllerMethodMetadata, + getControllerParameterMetadata, + instanceOfIHttpActionResult, +} from "./utils"; +import { + TYPE, + METADATA_KEY, + DEFAULT_ROUTING_ROOT_PATH, + PARAMETER_TYPE, + DUPLICATED_CONTROLLER_NAME, +} from "./constants"; +import { HttpResponseMessage } from "./httpResponseMessage"; + +import type { + AuthProvider, + ConfigFunction, + ControllerHandler, + ControllerMethodMetadata, + ExtractedParameters, + HttpContext, + Middleware, + ParameterMetadata, + Principal, + RoutingConfig, +} from "./interfaces"; +import type { OutgoingHttpHeaders } from "node:http"; -import express, { Application, NextFunction, Request, RequestHandler, Response, Router } from 'express'; -import { interfaces } from 'inversify'; -import { BaseMiddleware, Controller } from './index'; -import { getControllersFromMetadata, getControllersFromContainer, getControllerMetadata, getControllerMethodMetadata, getControllerParameterMetadata, instanceOfIHttpActionResult } from './utils'; -import { TYPE, METADATA_KEY, DEFAULT_ROUTING_ROOT_PATH, PARAMETER_TYPE, DUPLICATED_CONTROLLER_NAME, } from './constants'; -import { HttpResponseMessage } from './httpResponseMessage'; - -import type { AuthProvider, ConfigFunction, ControllerHandler, ControllerMethodMetadata, ExtractedParameters, HttpContext, Middleware, ParameterMetadata, Principal, RoutingConfig } from './interfaces'; -import type { OutgoingHttpHeaders } from 'node:http'; +export class InversifyExpressServer { + private _router: Router; + private _container: interfaces.Container; + private _app: Application; + private _configFn!: ConfigFunction; + private _errorConfigFn!: ConfigFunction; + private _routingConfig: RoutingConfig; + private _AuthProvider!: new () => AuthProvider; + private _forceControllers: boolean; + + /** + * Wrapper for the express server. + * + * @param container Container loaded with all controllers and their dependencies. + * @param customRouter optional express.Router custom router + * @param routingConfig optional interfaces.RoutingConfig routing config + * @param customApp optional express.Application custom app + * @param authProvider optional interfaces.AuthProvider auth provider + * @param forceControllers optional boolean setting to force controllers (defaults do true) + */ + constructor( + container: interfaces.Container, + customRouter?: Router | null, + routingConfig?: RoutingConfig | null, + customApp?: Application | null, + authProvider?: (new () => AuthProvider) | null, + forceControllers = true + ) { + this._container = container; + this._forceControllers = forceControllers; + this._router = customRouter || Router(); + this._routingConfig = routingConfig || { + rootPath: DEFAULT_ROUTING_ROOT_PATH, + }; + this._app = customApp || express(); + if (authProvider) { + this._AuthProvider = authProvider; + container.bind(TYPE.AuthProvider).to(this._AuthProvider); + } + } + /** + * Sets the configuration function to be applied to the application. + * Note that the config function is not actually executed until a call to + * InversifyExpresServer.build(). + * + * This method is chainable. + * + * @param fn Function in which app-level middleware can be registered. + */ + public setConfig(fn: ConfigFunction): InversifyExpressServer { + this._configFn = fn; + return this; + } -export class InversifyExpressServer { - private _router: Router; - private _container: interfaces.Container; - private _app: Application; - private _configFn!: ConfigFunction; - private _errorConfigFn!: ConfigFunction; - private _routingConfig: RoutingConfig; - private _AuthProvider!: new () => AuthProvider; - private _forceControllers: boolean; - - /** - * Wrapper for the express server. - * - * @param container Container loaded with all controllers and their dependencies. - * @param customRouter optional express.Router custom router - * @param routingConfig optional interfaces.RoutingConfig routing config - * @param customApp optional express.Application custom app - * @param authProvider optional interfaces.AuthProvider auth provider - * @param forceControllers optional boolean setting to force controllers (defaults do true) - */ - constructor( - container: interfaces.Container, - customRouter?: Router | null, - routingConfig?: RoutingConfig | null, - customApp?: Application | null, - authProvider?: (new () => AuthProvider) | null, - forceControllers = true, - ) { - this._container = container; - this._forceControllers = forceControllers; - this._router = customRouter || Router(); - this._routingConfig = routingConfig || { - rootPath: DEFAULT_ROUTING_ROOT_PATH, - }; - this._app = customApp || express(); - if (authProvider) { - this._AuthProvider = authProvider; - container.bind(TYPE.AuthProvider) - .to(this._AuthProvider); + /** + * Sets the error handler configuration function to be applied to the application. + * Note that the error config function is not actually executed until a call to + * InversifyExpresServer.build(). + * + * This method is chainable. + * + * @param fn Function in which app-level error handlers can be registered. + */ + public setErrorConfig(fn: ConfigFunction): InversifyExpressServer { + this._errorConfigFn = fn; + return this; } - } - - /** - * Sets the configuration function to be applied to the application. - * Note that the config function is not actually executed until a call to - * InversifyExpresServer.build(). - * - * This method is chainable. - * - * @param fn Function in which app-level middleware can be registered. - */ - public setConfig(fn: ConfigFunction): InversifyExpressServer { - this._configFn = fn; - return this; - } - - /** - * Sets the error handler configuration function to be applied to the application. - * Note that the error config function is not actually executed until a call to - * InversifyExpresServer.build(). - * - * This method is chainable. - * - * @param fn Function in which app-level error handlers can be registered. - */ - public setErrorConfig(fn: ConfigFunction): InversifyExpressServer { - this._errorConfigFn = fn; - return this; - } - - /** - * Applies all routes and configuration to the server, returning the express application. - */ - public build(): express.Application { - // The very first middleware to be invoked - // it creates a new httpContext and attaches it to the - // current request as metadata using Reflect - this._app.all('*', ( - req: Request, - res: Response, - next: NextFunction, - ) => { - void (async (): Promise => { - const httpContext = await this._createHttpContext(req, res, next); - Reflect.defineMetadata( - METADATA_KEY.httpContext, - httpContext, - req, - ); - next(); - })(); - }); - - // register server-level middleware before anything else - if (this._configFn) { - this._configFn.apply(undefined, [this._app]); + + /** + * Applies all routes and configuration to the server, returning the express application. + */ + public build(): express.Application { + // The very first middleware to be invoked + // it creates a new httpContext and attaches it to the + // current request as metadata using Reflect + this._app.all("*", (req: Request, res: Response, next: NextFunction) => { + void (async (): Promise => { + const httpContext = await this._createHttpContext(req, res, next); + Reflect.defineMetadata(METADATA_KEY.httpContext, httpContext, req); + next(); + })(); + }); + + // register server-level middleware before anything else + if (this._configFn) { + this._configFn.apply(undefined, [this._app]); + } + + this.registerControllers(); + + // register error handlers after controllers + if (this._errorConfigFn) { + this._errorConfigFn.apply(undefined, [this._app]); + } + + return this._app; } - this.registerControllers(); + private registerControllers(): void { + // Fake HttpContext is needed during registration + this._container.bind(TYPE.HttpContext).toConstantValue({} as HttpContext); + + const constructors = getControllersFromMetadata(); + + constructors.forEach((constructor) => { + const { name } = constructor as { name: string }; + + if (this._container.isBoundNamed(TYPE.Controller, name)) { + throw new Error(DUPLICATED_CONTROLLER_NAME(name)); + } - // register error handlers after controllers - if (this._errorConfigFn) { - this._errorConfigFn.apply(undefined, [this._app]); + this._container + .bind(TYPE.Controller) + .to(constructor as new (...args: never[]) => unknown) + .whenTargetNamed(name); + }); + + const controllers = getControllersFromContainer(this._container, this._forceControllers); + + controllers.forEach((controller: Controller) => { + const controllerMetadata = getControllerMetadata(controller.constructor); + const methodMetadata = getControllerMethodMetadata(controller.constructor); + const parameterMetadata = getControllerParameterMetadata(controller.constructor); + + if (controllerMetadata && methodMetadata) { + const controllerMiddleware = this.resolveMiddlewere( + ...controllerMetadata.middleware + ); + + methodMetadata.forEach((metadata: ControllerMethodMetadata) => { + let paramList: ParameterMetadata[] = []; + if (parameterMetadata) { + paramList = parameterMetadata[metadata.key] || []; + } + const handler: RequestHandler = this.handlerFactory( + (controllerMetadata.target as { name: string }).name, + metadata.key, + paramList + ); + + const routeMiddleware = this.resolveMiddlewere(...metadata.middleware); + this._router[metadata.method]( + `${controllerMetadata.path}${metadata.path}`, + ...controllerMiddleware, + ...routeMiddleware, + handler + ); + }); + } + }); + + this._app.use(this._routingConfig.rootPath, this._router); } - return this._app; - } - - private registerControllers(): void { - // Fake HttpContext is needed during registration - this._container - .bind(TYPE.HttpContext) - .toConstantValue({} as HttpContext); - - const constructors = getControllersFromMetadata(); - - constructors.forEach(constructor => { - const { name } = constructor as { name: string }; - - if (this._container.isBoundNamed(TYPE.Controller, name)) { - throw new Error(DUPLICATED_CONTROLLER_NAME(name)); - } - - this._container.bind(TYPE.Controller) - .to(constructor as new (...args: never[]) => unknown) - .whenTargetNamed(name); - }); - - const controllers = getControllersFromContainer( - this._container, - this._forceControllers, - ); - - controllers.forEach((controller: Controller) => { - const controllerMetadata = getControllerMetadata(controller.constructor); - const methodMetadata = getControllerMethodMetadata( - controller.constructor - ); - const parameterMetadata = getControllerParameterMetadata( - controller.constructor - ); - - if (controllerMetadata && methodMetadata) { - const controllerMiddleware = this.resolveMiddlewere( - ...controllerMetadata.middleware, - ); - - methodMetadata.forEach((metadata: ControllerMethodMetadata) => { - let paramList: ParameterMetadata[] = []; - if (parameterMetadata) { - paramList = parameterMetadata[metadata.key] || []; - } - const handler: RequestHandler = this.handlerFactory( - (controllerMetadata.target as { name: string }).name, - metadata.key, - paramList, - ); - - const routeMiddleware = this.resolveMiddlewere( - ...metadata.middleware - ); - this._router[metadata.method]( - `${controllerMetadata.path}${metadata.path}`, - ...controllerMiddleware, - ...routeMiddleware, - handler, - ); + private resolveMiddlewere(...middleware: Middleware[]): RequestHandler[] { + return middleware.map((middlewareItem) => { + if (!this._container.isBound(middlewareItem)) { + return middlewareItem as express.RequestHandler; + } + + type MiddlewareInstance = RequestHandler | BaseMiddleware; + const middlewareInstance = this._container.get(middlewareItem); + + if (middlewareInstance instanceof BaseMiddleware) { + return (req: Request, res: Response, next: NextFunction): void => { + const mReq = this._container.get(middlewareItem); + mReq.httpContext = this._getHttpContext(req); + mReq.handler(req, res, next); + }; + } + + return middlewareInstance; }); - } - }); - - this._app.use(this._routingConfig.rootPath, this._router); - } - - private resolveMiddlewere( - ...middleware: Middleware[] - ): RequestHandler[] { - return middleware.map(middlewareItem => { - if (!this._container.isBound(middlewareItem)) { - return middlewareItem as express.RequestHandler; - } - - type MiddlewareInstance = RequestHandler | BaseMiddleware; - const middlewareInstance = this._container - .get(middlewareItem); - - if (middlewareInstance instanceof BaseMiddleware) { - return ( - req: Request, - res: Response, - next: NextFunction, - ): void => { - const mReq = this._container.get(middlewareItem); - mReq.httpContext = this._getHttpContext(req); - mReq.handler(req, res, next); + } + + private copyHeadersTo(headers: OutgoingHttpHeaders, target: Response): void { + for (const name of Object.keys(headers)) { + const headerValue = headers[name]; + + target.append( + name, + typeof headerValue === "number" ? headerValue.toString() : headerValue + ); + } + } + + private async handleHttpResponseMessage( + message: HttpResponseMessage, + res: express.Response + ): Promise { + this.copyHeadersTo(message.headers, res); + + if (message.content !== undefined) { + this.copyHeadersTo(message.content.headers, res); + + res.status(message.statusCode) + // If the content is a number, ensure we change it to a string, else our content is + // treated as a statusCode rather than as the content of the Response + .send(await message.content.readAsync()); + } else { + res.sendStatus(message.statusCode); + } + } + + private handlerFactory( + controllerName: string, + key: string, + parameterMetadata: ParameterMetadata[] + ): RequestHandler { + return async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const args = this.extractParameters(req, res, next, parameterMetadata); + const httpContext = this._getHttpContext(req); + httpContext.container + .bind(TYPE.HttpContext) + .toConstantValue(httpContext); + + // invoke controller's action + const value = await ( + httpContext.container.getNamed(TYPE.Controller, controllerName)[ + key + ] as ControllerHandler + )(...args); + + if (value instanceof HttpResponseMessage) { + await this.handleHttpResponseMessage(value, res); + } else if (instanceOfIHttpActionResult(value)) { + const httpResponseMessage = await value.executeAsync(); + await this.handleHttpResponseMessage(httpResponseMessage, res); + } else if (value instanceof Function) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + value(); + } else if (!res.headersSent) { + if (value !== undefined) { + res.send(value); + } + } + } catch (err) { + next(err); + } }; - } - - return middlewareInstance; - }); - } - - private copyHeadersTo( - headers: OutgoingHttpHeaders, - target: Response - ): void { - for (const name of Object.keys(headers)) { - const headerValue = headers[name]; - - target.append( - name, - typeof headerValue === 'number' ? headerValue.toString() : headerValue, - ); } - } - - private async handleHttpResponseMessage( - message: HttpResponseMessage, - res: express.Response, - ): Promise { - this.copyHeadersTo(message.headers, res); - - if (message.content !== undefined) { - this.copyHeadersTo(message.content.headers, res); - - res.status(message.statusCode) - // If the content is a number, ensure we change it to a string, else our content is - // treated as a statusCode rather than as the content of the Response - .send(await message.content.readAsync()); - } else { - res.sendStatus(message.statusCode); + + private _getHttpContext(req: express.Request): HttpContext { + return Reflect.getMetadata(METADATA_KEY.httpContext, req) as HttpContext; } - } - - private handlerFactory( - controllerName: string, - key: string, - parameterMetadata: ParameterMetadata[], - ): RequestHandler { - return async ( - req: Request, - res: Response, - next: NextFunction - ): Promise => { - try { - const args = this.extractParameters(req, res, next, parameterMetadata); - const httpContext = this._getHttpContext(req); - httpContext.container.bind(TYPE.HttpContext) - .toConstantValue(httpContext); - - // invoke controller's action - const value = await (httpContext.container.getNamed( - TYPE.Controller, - controllerName, - )[key] as ControllerHandler)(...args); - - if (value instanceof HttpResponseMessage) { - await this.handleHttpResponseMessage(value, res); - } else if (instanceOfIHttpActionResult(value)) { - const httpResponseMessage = await value.executeAsync(); - await this.handleHttpResponseMessage(httpResponseMessage, res); - } else if (value instanceof Function) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - value(); - } else if (!res.headersSent) { - if (value === undefined) { - res.status(204); - } - res.send(value); + + private async _createHttpContext( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const principal = await this._getCurrentUser(req, res, next); + return { + // We use a childContainer for each request so we can be + // sure that the binding is unique for each HTTP request + container: this._container.createChild(), + request: req, + response: res, + user: principal, + }; + } + + private async _getCurrentUser( + req: Request, + res: Response, + next: NextFunction + ): Promise { + if (this._AuthProvider !== undefined) { + const authProvider = this._container.get(TYPE.AuthProvider); + return authProvider.getUser(req, res, next); } - } catch (err) { - next(err); - } - }; - } - - private _getHttpContext(req: express.Request): HttpContext { - return Reflect.getMetadata( - METADATA_KEY.httpContext, - req, - ) as HttpContext; - } - - private async _createHttpContext( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - const principal = await this._getCurrentUser(req, res, next); - return { - // We use a childContainer for each request so we can be - // sure that the binding is unique for each HTTP request - container: this._container.createChild(), - request: req, - response: res, - user: principal - }; - } - - private async _getCurrentUser( - req: Request, - res: Response, - next: NextFunction, - ): Promise { - if (this._AuthProvider !== undefined) { - const authProvider = this._container.get(TYPE.AuthProvider); - return authProvider.getUser(req, res, next); + return Promise.resolve({ + details: null, + isAuthenticated: () => Promise.resolve(false), + isInRole: (_role: string) => Promise.resolve(false), + isResourceOwner: (_resourceId: unknown) => Promise.resolve(false), + }); } - return Promise.resolve({ - details: null, - isAuthenticated: () => Promise.resolve(false), - isInRole: (_role: string) => Promise.resolve(false), - isResourceOwner: (_resourceId: unknown) => Promise.resolve(false), - }); - } - - private extractParameters( - req: Request, - res: Response, - next: NextFunction, - params: ParameterMetadata[], - ): ExtractedParameters { - const args: unknown[] = []; - if (!params || !params.length) { - return [req, res, next]; + + private extractParameters( + req: Request, + res: Response, + next: NextFunction, + params: ParameterMetadata[] + ): ExtractedParameters { + const args: unknown[] = []; + if (!params || !params.length) { + return [req, res, next]; + } + + params.forEach(({ type, index, parameterName, injectRoot }) => { + switch (type) { + case PARAMETER_TYPE.REQUEST: + args[index] = req; + break; + case PARAMETER_TYPE.NEXT: + args[index] = next; + break; + case PARAMETER_TYPE.PARAMS: + args[index] = this.getParam(req, "params", injectRoot, parameterName); + break; + case PARAMETER_TYPE.QUERY: + args[index] = this.getParam(req, "query", injectRoot, parameterName); + break; + case PARAMETER_TYPE.BODY: + args[index] = req.body; + break; + case PARAMETER_TYPE.HEADERS: + args[index] = this.getParam(req, "headers", injectRoot, parameterName); + break; + case PARAMETER_TYPE.COOKIES: + args[index] = this.getParam(req, "cookies", injectRoot, parameterName); + break; + case PARAMETER_TYPE.PRINCIPAL: + args[index] = this._getPrincipal(req); + break; + default: + args[index] = res; + break; // response + } + }); + + args.push(req, res, next); + return args; } - params.forEach(({ - type, index, parameterName, injectRoot, - }) => { - switch (type) { - case PARAMETER_TYPE.REQUEST: - args[index] = req; - break; - case PARAMETER_TYPE.NEXT: - args[index] = next; - break; - case PARAMETER_TYPE.PARAMS: - args[index] = this.getParam(req, 'params', injectRoot, parameterName); - break; - case PARAMETER_TYPE.QUERY: - args[index] = this.getParam(req, 'query', injectRoot, parameterName); - break; - case PARAMETER_TYPE.BODY: - args[index] = req.body; - break; - case PARAMETER_TYPE.HEADERS: - args[index] = this.getParam( - req, - 'headers', - injectRoot, - parameterName - ); - break; - case PARAMETER_TYPE.COOKIES: - args[index] = this.getParam( - req, - 'cookies', - injectRoot, - parameterName - ); - break; - case PARAMETER_TYPE.PRINCIPAL: - args[index] = this._getPrincipal(req); - break; - default: - args[index] = res; - break; // response - } - }); - - args.push(req, res, next); - return args; - } - - private getParam( - source: Request, - paramType: 'params' | 'query' | 'headers' | 'cookies', - injectRoot: boolean, - name?: string | symbol, - ): unknown { - const key = paramType === 'headers' ? - typeof name === 'symbol' ? - name.toString() : - name?.toLowerCase() : - name as string; - const param = source[paramType] as Record; - - if (injectRoot) { - return param; + private getParam( + source: Request, + paramType: "params" | "query" | "headers" | "cookies", + injectRoot: boolean, + name?: string | symbol + ): unknown { + const key = + paramType === "headers" + ? typeof name === "symbol" + ? name.toString() + : name?.toLowerCase() + : (name as string); + const param = source[paramType] as Record; + + if (injectRoot) { + return param; + } + return param && key ? param[key] : undefined; } - return (param && key) ? param[key] : undefined; - } - private _getPrincipal(req: express.Request): Principal | null { - return this._getHttpContext(req).user; - } -} \ No newline at end of file + private _getPrincipal(req: express.Request): Principal | null { + return this._getHttpContext(req).user; + } +} diff --git a/test/server.test.ts b/test/server.test.ts index 9d88dec..5225f8d 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -1,823 +1,812 @@ -import supertest from 'supertest'; -import { interfaces } from 'inversify'; -import { Request, Response, Router, NextFunction, RequestHandler, json, CookieOptions } from 'express'; -import cookieParser from 'cookie-parser'; -import { injectable, Container } from 'inversify'; -import { AuthProvider, InversifyExpressServer, Principal } from '../src'; -import { controller, httpMethod, all, httpGet, httpPost, httpPut, httpPatch, httpHead, httpDelete, request, response, requestParam, requestBody, queryParam, requestHeaders, cookies, next, principal } from '../src/decorators'; -import { cleanUpMetadata } from '../src/utils'; - -describe('Integration Tests:', () => { - let server: InversifyExpressServer; - let container: interfaces.Container; - - beforeEach(done => { - cleanUpMetadata(); - container = new Container(); - done(); - }); - - describe('Routing & Request Handling:', () => { - it('should work for async controller methods', done => { - @controller('/') - class TestController { - @httpGet('/') public getTest(req: Request, res: Response) { - return new Promise((resolve => { - setTimeout(resolve, 100, 'GET'); - })); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'GET', done); - }); +import supertest from "supertest"; +import { interfaces } from "inversify"; +import { + Request, + Response, + Router, + NextFunction, + RequestHandler, + json, + CookieOptions, +} from "express"; +import cookieParser from "cookie-parser"; +import { injectable, Container } from "inversify"; +import { AuthProvider, InversifyExpressServer, Principal } from "../src"; +import { + controller, + httpMethod, + all, + httpGet, + httpPost, + httpPut, + httpPatch, + httpHead, + httpDelete, + request, + response, + requestParam, + requestBody, + queryParam, + requestHeaders, + cookies, + next, + principal, +} from "../src/decorators"; +import { cleanUpMetadata } from "../src/utils"; + +describe("Integration Tests:", () => { + let server: InversifyExpressServer; + let container: interfaces.Container; + + beforeEach((done) => { + cleanUpMetadata(); + container = new Container(); + done(); + }); + + describe("Routing & Request Handling:", () => { + it("should work for async controller methods", (done) => { + @controller("/") + class TestController { + @httpGet("/") public getTest(req: Request, res: Response) { + return new Promise((resolve) => { + setTimeout(resolve, 100, "GET"); + }); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "GET", done); + }); - it('should work for async controller methods that fails', done => { - @controller('/') - class TestController { - @httpGet('/') public getTest(req: Request, res: Response) { - return new Promise(((resolve, reject) => { - setTimeout(reject, 100, 'GET'); - })); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(500, done); - }); + it("should work for async controller methods that fails", (done) => { + @controller("/") + class TestController { + @httpGet("/") public getTest(req: Request, res: Response) { + return new Promise((resolve, reject) => { + setTimeout(reject, 100, "GET"); + }); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(500, done); + }); - it('should work for methods which call nextFunc()', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response, nextFunc: NextFunction) { - nextFunc(); - } - - @httpGet('/') public getTest2(req: Request, res: Response) { - return 'GET'; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'GET', done); - }); + it("should work for methods which call nextFunc()", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response, nextFunc: NextFunction) { + nextFunc(); + } + + @httpGet("/") public getTest2(req: Request, res: Response) { + return "GET"; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "GET", done); + }); - it('should work for async methods which call nextFunc()', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response, nextFunc: NextFunction) { - return new Promise((resolve => { - setTimeout(() => { - nextFunc(); - resolve(null); - }, 100, 'GET'); - })); - } - - @httpGet('/') public getTest2(req: Request, res: Response) { - return 'GET'; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'GET', done); - }); + it("should work for async methods which call nextFunc()", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response, nextFunc: NextFunction) { + return new Promise((resolve) => { + setTimeout( + () => { + nextFunc(); + resolve(null); + }, + 100, + "GET" + ); + }); + } + + @httpGet("/") public getTest2(req: Request, res: Response) { + return "GET"; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "GET", done); + }); - it('should work for async methods called by nextFunc()', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response, nextFunc: NextFunction) { - return nextFunc; - } - - @httpGet('/') public getTest2(req: Request, res: Response) { - return new Promise((resolve => { - setTimeout(resolve, 100, 'GET'); - })); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'GET', done); - }); + it("should work for async methods called by nextFunc()", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response, nextFunc: NextFunction) { + return nextFunc; + } + + @httpGet("/") public getTest2(req: Request, res: Response) { + return new Promise((resolve) => { + setTimeout(resolve, 100, "GET"); + }); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "GET", done); + }); - it('should work for each shortcut decorator', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response) { res.send('GET'); } + it("should work for each shortcut decorator", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response) { + res.send("GET"); + } + + @httpPost("/") + public postTest(req: Request, res: Response) { + res.send("POST"); + } + + @httpPut("/") + public putTest(req: Request, res: Response) { + res.send("PUT"); + } + + @httpPatch("/") + public patchTest(req: Request, res: Response) { + res.send("PATCH"); + } + + @httpHead("/") + public headTest(req: Request, res: Response) { + res.send("HEAD"); + } + + @httpDelete("/") + public deleteTest(req: Request, res: Response) { + res.send("DELETE"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + const deleteFn = () => { + void agent.delete("/").expect(200, "DELETE", done); + }; + const head = () => { + void agent.head("/").expect(200, "HEAD", deleteFn); + }; + const patch = () => { + void agent.patch("/").expect(200, "PATCH", head); + }; + const put = () => { + void agent.put("/").expect(200, "PUT", patch); + }; + const post = () => { + void agent.post("/").expect(200, "POST", put); + }; + const get = () => { + void agent.get("/").expect(200, "GET", post); + }; + + get(); + }); - @httpPost('/') - public postTest(req: Request, res: Response) { res.send('POST'); } + it("should work for more obscure HTTP methods using the httpMethod decorator", (done) => { + @controller("/") + class TestController { + @httpMethod("propfind", "/") + public getTest(req: Request, res: Response) { + res.send("PROPFIND"); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).propfind("/").expect(200, "PROPFIND", done); + }); - @httpPut('/') - public putTest(req: Request, res: Response) { res.send('PUT'); } + it("should use returned values as response", (done) => { + const result = { hello: "world" }; - @httpPatch('/') - public patchTest(req: Request, res: Response) { res.send('PATCH'); } + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response) { + return result; + } + } - @httpHead('/') - public headTest(req: Request, res: Response) { res.send('HEAD'); } + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, JSON.stringify(result), done); + }); - @httpDelete('/') - public deleteTest(req: Request, res: Response) { res.send('DELETE'); } - } + it("should use custom router passed from configuration", () => { + @controller("/CaseSensitive") + class TestController { + @httpGet("/Endpoint") public get() { + return "Such Text"; + } + } - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); + const customRouter = Router({ + caseSensitive: true, + }); - const deleteFn = () => { - void agent.delete('/').expect(200, 'DELETE', done); - }; - const head = () => { - void agent.head('/').expect(200, 'HEAD', deleteFn); - }; - const patch = () => { void agent.patch('/').expect(200, 'PATCH', head); }; - const put = () => { void agent.put('/').expect(200, 'PUT', patch); }; - const post = () => { void agent.post('/').expect(200, 'POST', put); }; - const get = () => { void agent.get('/').expect(200, 'GET', post); }; + server = new InversifyExpressServer(container, customRouter); + const app = server.build(); - get(); - }); + const expectedSuccess = supertest(app) + .get("/CaseSensitive/Endpoint") + .expect(200, "Such Text"); - it('should work for more obscure HTTP methods using the httpMethod decorator', done => { - @controller('/') - class TestController { - @httpMethod('propfind', '/') - public getTest(req: Request, res: Response) { - res.send('PROPFIND'); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .propfind('/') - .expect(200, 'PROPFIND', done); - }); + const expectedNotFound1 = supertest(app).get("/casesensitive/endpoint").expect(404); - it('should use returned values as response', done => { - const result = { hello: 'world' }; - - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response) { - return result; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, JSON.stringify(result), done); - }); + const expectedNotFound2 = supertest(app).get("/CaseSensitive/endpoint").expect(404); - it('should use custom router passed from configuration', () => { - @controller('/CaseSensitive') - class TestController { - @httpGet('/Endpoint') public get() { - return 'Such Text'; - } - } - - const customRouter = Router({ - caseSensitive: true, - }); - - server = new InversifyExpressServer(container, customRouter); - const app = server.build(); - - const expectedSuccess = supertest(app) - .get('/CaseSensitive/Endpoint') - .expect(200, 'Such Text'); - - const expectedNotFound1 = supertest(app) - .get('/casesensitive/endpoint') - .expect(404); - - const expectedNotFound2 = supertest(app) - .get('/CaseSensitive/endpoint') - .expect(404); - - return Promise.all([ - expectedSuccess, - expectedNotFound1, - expectedNotFound2, - ]); - }); + return Promise.all([expectedSuccess, expectedNotFound1, expectedNotFound2]); + }); - it('should use custom routing configuration', () => { - @controller('/ping') - class TestController { - @httpGet('/endpoint') public get() { - return 'pong'; - } - } - - server = new InversifyExpressServer( - container, - null, - { rootPath: '/api/v1' } - ); - - return supertest(server.build()) - .get('/api/v1/ping/endpoint') - .expect(200, 'pong'); - }); + it("should use custom routing configuration", () => { + @controller("/ping") + class TestController { + @httpGet("/endpoint") public get() { + return "pong"; + } + } - it('should work for controller methods who\'s return value is falsey', done => { - @controller('/user') - class TestController { - @httpDelete('/') public async delete(): Promise { - // - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .delete('/user') - .expect(204, '', done); - }); - }); - - describe('Middleware:', () => { - let result: string; - type Middleware = { - a: (req: Request, res: Response, nextFunc: NextFunction) => void; - b: (req: Request, res: Response, nextFunc: NextFunction) => void; - c: (req: Request, res: Response, nextFunc: NextFunction) => void; - }; - const middleware: Middleware = { - a(req: Request, res: Response, nextFunc: NextFunction) { - result += 'a'; - nextFunc(); - }, - b(req: Request, res: Response, nextFunc: NextFunction) { - result += 'b'; - nextFunc(); - }, - c(req: Request, res: Response, nextFunc: NextFunction) { - result += 'c'; - nextFunc(); - }, - }; - - const spyA = jest.fn().mockImplementation(middleware.a); - const spyB = jest.fn().mockImplementation(middleware.b); - const spyC = jest.fn().mockImplementation(middleware.c); - - beforeEach(done => { - spyA.mockClear(); - spyB.mockClear(); - spyC.mockClear(); - result = ''; - done(); - }); + server = new InversifyExpressServer(container, null, { rootPath: "/api/v1" }); - it('should call method-level middleware correctly (GET)', done => { - @controller('/') - class TestController { - @httpGet('/', spyA, spyB, spyC) - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.get('/') - .expect(200, 'GET', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + return supertest(server.build()).get("/api/v1/ping/endpoint").expect(200, "pong"); }); - }); - it('should call method-level middleware correctly (POST)', done => { - @controller('/') - class TestController { - @httpPost('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('POST'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.post('/') - .expect(200, 'POST', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should work for controller methods who's return value is falsey", (done) => { + @controller("/user") + class TestController { + @httpDelete("/") public async delete(): Promise { + // + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()) + .delete("/user") + .timeout({ response: 1000, deadline: 2000 }) + .end((err, res) => { + if (err) { + if (err.timeout) { + // The request timed out as expected because no response was sent + done(); + } else { + // Some other error occurred + done(err); + } + } else { + // If we get here, the request did not hang, and a response was received + done(new Error("Expected request to hang, but a response was received")); + } + }); }); }); - it('should call method-level middleware correctly (PUT)', done => { - @controller('/') - class TestController { - @httpPut('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('PUT'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.put('/') - .expect(200, 'PUT', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + describe("Middleware:", () => { + let result: string; + type Middleware = { + a: (req: Request, res: Response, nextFunc: NextFunction) => void; + b: (req: Request, res: Response, nextFunc: NextFunction) => void; + c: (req: Request, res: Response, nextFunc: NextFunction) => void; + }; + const middleware: Middleware = { + a(req: Request, res: Response, nextFunc: NextFunction) { + result += "a"; + nextFunc(); + }, + b(req: Request, res: Response, nextFunc: NextFunction) { + result += "b"; + nextFunc(); + }, + c(req: Request, res: Response, nextFunc: NextFunction) { + result += "c"; + nextFunc(); + }, + }; + + const spyA = jest.fn().mockImplementation(middleware.a); + const spyB = jest.fn().mockImplementation(middleware.b); + const spyC = jest.fn().mockImplementation(middleware.c); + + beforeEach((done) => { + spyA.mockClear(); + spyB.mockClear(); + spyC.mockClear(); + result = ""; + done(); }); - }); - it('should call method-level middleware correctly (PATCH)', done => { - @controller('/') - class TestController { - @httpPatch('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('PATCH'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.patch('/') - .expect(200, 'PATCH', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (GET)", (done) => { + @controller("/") + class TestController { + @httpGet("/", spyA, spyB, spyC) + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.get("/").expect(200, "GET", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call method-level middleware correctly (HEAD)', done => { - @controller('/') - class TestController { - @httpHead('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('HEAD'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.head('/') - .expect(200, 'HEAD', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (POST)", (done) => { + @controller("/") + class TestController { + @httpPost("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("POST"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.post("/").expect(200, "POST", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call method-level middleware correctly (DELETE)', done => { - @controller('/') - class TestController { - @httpDelete('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('DELETE'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.delete('/') - .expect(200, 'DELETE', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (PUT)", (done) => { + @controller("/") + class TestController { + @httpPut("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("PUT"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.put("/").expect(200, "PUT", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call method-level middleware correctly (ALL)', done => { - @controller('/') - class TestController { - @all('/', spyA, spyB, spyC) - public postTest(req: Request, res: Response) { - res.send('ALL'); - } - } - - server = new InversifyExpressServer(container); - const agent = supertest(server.build()); - - void agent.get('/') - .expect(200, 'ALL', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (PATCH)", (done) => { + @controller("/") + class TestController { + @httpPatch("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("PATCH"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.patch("/").expect(200, "PATCH", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call controller-level middleware correctly', done => { - @controller('/', spyA, spyB, spyC) - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'GET', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (HEAD)", (done) => { + @controller("/") + class TestController { + @httpHead("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("HEAD"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.head("/").expect(200, "HEAD", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call server-level middleware correctly', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } - - server = new InversifyExpressServer(container); - - server.setConfig(app => { - app.use(spyA); - app.use(spyB); - app.use(spyC); - }); - - void supertest(server.build()) - .get('/') - .expect(200, 'GET', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (DELETE)", (done) => { + @controller("/") + class TestController { + @httpDelete("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("DELETE"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.delete("/").expect(200, "DELETE", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should call all middleware in correct order', done => { - @controller('/', spyB) - class TestController { - @httpGet('/', spyC) - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } - - server = new InversifyExpressServer(container); - - server.setConfig(app => { - app.use(spyA); - }); - - void supertest(server.build()) - .get('/') - .expect(200, 'GET', () => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(spyC).toHaveBeenCalledTimes(1); - expect(result).toBe('abc'); - done(); + it("should call method-level middleware correctly (ALL)", (done) => { + @controller("/") + class TestController { + @all("/", spyA, spyB, spyC) + public postTest(req: Request, res: Response) { + res.send("ALL"); + } + } + + server = new InversifyExpressServer(container); + const agent = supertest(server.build()); + + void agent.get("/").expect(200, "ALL", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - - it('should resolve controller-level middleware', () => { - const symbolId = Symbol.for('spyA'); - const strId = 'spyB'; - - @controller('/', symbolId, strId) - class TestController { - @httpGet('/') - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } - container.bind(symbolId).toConstantValue(spyA); - container.bind(strId).toConstantValue(spyB); - - server = new InversifyExpressServer(container); - - const agent = supertest(server.build()); - - return agent.get('/') - .expect(200, 'GET') - .then(() => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(result).toBe('ab'); + it("should call controller-level middleware correctly", (done) => { + @controller("/", spyA, spyB, spyC) + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()) + .get("/") + .expect(200, "GET", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); }); - }); - it('should resolve method-level middleware', () => { - const symbolId = Symbol.for('spyA'); - const strId = 'spyB'; - - @controller('/') - class TestController { - @httpGet('/', symbolId, strId) - public getTest(req: Request, res: Response) { - res.send('GET'); - } - } + it("should call server-level middleware correctly", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + server = new InversifyExpressServer(container); + + server.setConfig((app) => { + app.use(spyA); + app.use(spyB); + app.use(spyC); + }); + + void supertest(server.build()) + .get("/") + .expect(200, "GET", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); + }); - container.bind(symbolId).toConstantValue(spyA); - container.bind(strId).toConstantValue(spyB); + it("should call all middleware in correct order", (done) => { + @controller("/", spyB) + class TestController { + @httpGet("/", spyC) + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + server = new InversifyExpressServer(container); + + server.setConfig((app) => { + app.use(spyA); + }); + + void supertest(server.build()) + .get("/") + .expect(200, "GET", () => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(spyC).toHaveBeenCalledTimes(1); + expect(result).toBe("abc"); + done(); + }); + }); - server = new InversifyExpressServer(container); + it("should resolve controller-level middleware", () => { + const symbolId = Symbol.for("spyA"); + const strId = "spyB"; + + @controller("/", symbolId, strId) + class TestController { + @httpGet("/") + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + container.bind(symbolId).toConstantValue(spyA); + container.bind(strId).toConstantValue(spyB); + + server = new InversifyExpressServer(container); + + const agent = supertest(server.build()); + + return agent + .get("/") + .expect(200, "GET") + .then(() => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(result).toBe("ab"); + }); + }); - const agent = supertest(server.build()); + it("should resolve method-level middleware", () => { + const symbolId = Symbol.for("spyA"); + const strId = "spyB"; + + @controller("/") + class TestController { + @httpGet("/", symbolId, strId) + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + container.bind(symbolId).toConstantValue(spyA); + container.bind(strId).toConstantValue(spyB); + + server = new InversifyExpressServer(container); + + const agent = supertest(server.build()); + + return agent + .get("/") + .expect(200, "GET") + .then(() => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(result).toBe("ab"); + }); + }); - return agent.get('/') - .expect(200, 'GET') - .then(() => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(result).toBe('ab'); + it("should compose controller- and method-level middleware", () => { + const symbolId = Symbol.for("spyA"); + const strId = "spyB"; + + @controller("/", symbolId) + class TestController { + @httpGet("/", strId) + public getTest(req: Request, res: Response) { + res.send("GET"); + } + } + + container.bind(symbolId).toConstantValue(spyA); + container.bind(strId).toConstantValue(spyB); + + server = new InversifyExpressServer(container); + + const agent = supertest(server.build()); + + return agent + .get("/") + .expect(200, "GET") + .then(() => { + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + expect(result).toBe("ab"); + }); }); }); - it('should compose controller- and method-level middleware', () => { - const symbolId = Symbol.for('spyA'); - const strId = 'spyB'; - - @controller('/', symbolId) - class TestController { - @httpGet('/', strId) - public getTest(req: Request, res: Response) { res.send('GET'); } - } + describe("Parameters:", () => { + it("should bind a method parameter to the url parameter of the web request", (done) => { + @controller("/") + class TestController { + @httpGet(":id") + public getTest(@requestParam("id") id: string, req: Request, res: Response) { + return id; + } + } - container.bind(symbolId).toConstantValue(spyA); - container.bind(strId).toConstantValue(spyB); - - server = new InversifyExpressServer(container); + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/foo").expect(200, "foo", done); + }); - const agent = supertest(server.build()); + it("should bind a method parameter to the request object", (done) => { + @controller("/") + class TestController { + @httpGet(":id") + public getTest(@request() req: Request) { + return req.params["id"]; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/GET").expect(200, "GET", done); + }); - return agent.get('/') - .expect(200, 'GET') - .then(() => { - expect(spyA).toHaveBeenCalledTimes(1); - expect(spyB).toHaveBeenCalledTimes(1); - expect(result).toBe('ab'); + it("should bind a method parameter to the response object", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(@response() res: Response) { + return res.send("foo"); + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "foo", done); }); - }); - }); - - describe('Parameters:', () => { - it('should bind a method parameter to the url parameter of the web request', done => { - @controller('/') - class TestController { - @httpGet(':id') - public getTest( - @requestParam('id') id: string, - req: Request, - res: Response - ) { - return id; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/foo') - .expect(200, 'foo', done); - }); - it('should bind a method parameter to the request object', done => { - @controller('/') - class TestController { - @httpGet(':id') - public getTest( - @request() req: Request - ) { - return req.params['id']; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/GET') - .expect(200, 'GET', done); - }); + it("should bind a method parameter to a query parameter", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(@queryParam("id") id: string) { + return id; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").query("id=foo").expect(200, "foo", done); + }); - it('should bind a method parameter to the response object', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest( - @response() res: Response - ) { - return res.send('foo'); - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'foo', done); - }); + it("should bind a method parameter to the request body", (done) => { + @controller("/") + class TestController { + @httpPost("/") public getTest(@requestBody() reqBody: string) { + return reqBody; + } + } + + server = new InversifyExpressServer(container); + const body = { foo: "bar" }; + server.setConfig((app) => { + app.use(json()); + }); + void supertest(server.build()).post("/").send(body).expect(200, body, done); + }); - it('should bind a method parameter to a query parameter', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest( - @queryParam('id') id: string - ) { - return id; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .query('id=foo') - .expect(200, 'foo', done); - }); + it("should bind a method parameter to the request headers", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(@requestHeaders("testhead") headers: Record) { + return headers; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").set("TestHead", "foo").expect(200, "foo", done); + }); - it('should bind a method parameter to the request body', done => { - @controller('/') - class TestController { - @httpPost('/') public getTest(@requestBody() reqBody: string) { - return reqBody; - } - } - - server = new InversifyExpressServer(container); - const body = { foo: 'bar' }; - server.setConfig(app => { - app.use(json()); - }); - void supertest(server.build()) - .post('/') - .send(body) - .expect(200, body, done); - }); + it("should be case insensitive to request headers", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getTest(@requestHeaders("TestHead") headers: Record) { + return headers; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").set("TestHead", "foo").expect(200, "foo", done); + }); - it('should bind a method parameter to the request headers', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest( - @requestHeaders('testhead') headers: Record) { - return headers; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .set('TestHead', 'foo') - .expect(200, 'foo', done); - }); + it("should bind a method parameter to a cookie", (done) => { + @controller("/") + class TestController { + @httpGet("/") public getCookie( + @cookies("Cookie") cookie: CookieOptions, + req: Request, + res: Response + ) { + return cookie; + } + } + + server = new InversifyExpressServer(container); + server.setConfig((app) => { + app.use(cookieParser()); + }); + void supertest(server.build()) + .get("/") + .set("Cookie", "Cookie=hey") + .expect(200, "hey", done); + }); - it('should be case insensitive to request headers', done => { - @controller('/') - class TestController { - @httpGet('/') - public getTest( - @requestHeaders('TestHead') headers: Record) { - return headers; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .set('TestHead', 'foo') - .expect(200, 'foo', done); - }); + it("should bind a method parameter to the next function", (done) => { + @controller("/") + class TestController { + @httpGet("/") public getTest(@next() nextFunc: NextFunction) { + return nextFunc(); + } - it('should bind a method parameter to a cookie', done => { - @controller('/') - class TestController { - @httpGet('/') public getCookie( - @cookies('Cookie') cookie: CookieOptions, - req: Request, - res: Response - ) { - return cookie; - } - } - - server = new InversifyExpressServer(container); - server.setConfig(app => { - app.use(cookieParser()); - }); - void supertest(server.build()) - .get('/') - .set('Cookie', 'Cookie=hey') - .expect(200, 'hey', done); - }); + @httpGet("/") public getResult() { + return "foo"; + } + } - it('should bind a method parameter to the next function', done => { - @controller('/') - class TestController { - @httpGet('/') public getTest(@next() nextFunc: NextFunction) { - return nextFunc(); - } - - @httpGet('/') public getResult() { - return 'foo'; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, 'foo', done); - }); + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "foo", done); + }); - it('should bind a method parameter to a principal with null (empty) details when no AuthProvider is set.', done => { - @controller('/') - class TestController { - @httpGet('/') - public getPrincipalTest( - @principal() userPrincipal: Principal - ) { - return userPrincipal.details; - } - } - - server = new InversifyExpressServer(container); - void supertest(server.build()) - .get('/') - .expect(200, '', done); - }); + it("should bind a method parameter to a principal with null (empty) details when no AuthProvider is set.", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getPrincipalTest(@principal() userPrincipal: Principal) { + return userPrincipal.details; + } + } + + server = new InversifyExpressServer(container); + void supertest(server.build()).get("/").expect(200, "", done); + }); - it('should bind a method parameter to a principal with valid details when an AuthProvider is set.', done => { - @controller('/') - class TestController { - @httpGet('/') - public getPrincipalTest( - @principal() userPrincipal: Principal - ) { - return userPrincipal.details; - } - } - - @injectable() - class CustomAuthProvider implements AuthProvider { - public async getUser( - req: Request, - res: Response, - nextFunc: NextFunction, - ): Promise { - return Promise.resolve({ - details: 'something', - isAuthenticated: () => Promise.resolve(true), - isInRole: () => Promise.resolve(true), - isResourceOwner: () => Promise.resolve(true) - } as Principal); - } - } - - server = new InversifyExpressServer( - container, - null, - null, - null, - CustomAuthProvider - ); - void supertest(server.build()) - .get('/') - .expect(200, 'something', done); + it("should bind a method parameter to a principal with valid details when an AuthProvider is set.", (done) => { + @controller("/") + class TestController { + @httpGet("/") + public getPrincipalTest(@principal() userPrincipal: Principal) { + return userPrincipal.details; + } + } + + @injectable() + class CustomAuthProvider implements AuthProvider { + public async getUser( + req: Request, + res: Response, + nextFunc: NextFunction + ): Promise { + return Promise.resolve({ + details: "something", + isAuthenticated: () => Promise.resolve(true), + isInRole: () => Promise.resolve(true), + isResourceOwner: () => Promise.resolve(true), + } as Principal); + } + } + + server = new InversifyExpressServer(container, null, null, null, CustomAuthProvider); + void supertest(server.build()).get("/").expect(200, "something", done); + }); }); - }); -}); \ No newline at end of file +});