diff --git a/docs/site/Sequence.md b/docs/site/Sequence.md index 119b53667a9b..fcae1936f2f7 100644 --- a/docs/site/Sequence.md +++ b/docs/site/Sequence.md @@ -85,7 +85,7 @@ Here is an example where the application logs out a message before and after a request is handled: ```ts -import {DefaultSequence, ParsedRequest, ServerResponse} from '@loopback/rest'; +import {DefaultSequence, Request, Response} from '@loopback/rest'; class MySequence extends DefaultSequence { log(msg: string) { @@ -137,7 +137,7 @@ function upon injection. **custom-send.provider.ts** ```ts -import {Send, ServerResponse} from '@loopback/rest'; +import {Send, Response} from '@loopback/rest'; import {Provider, BoundValue, inject} from '@loopback/context'; import { writeResultToResponse, @@ -157,19 +157,18 @@ export class CustomSendProvider implements Provider { value() { // Use the lambda syntax to preserve the "this" scope for future calls! - return (response: ServerResponse, result: OperationRetval) => { + return (response: Response, result: OperationRetval) => { this.action(response, result); }; } /** * Use the mimeType given in the request's Accept header to convert * the response object! - * @param ServerResponse response The response object used to reply to the - * client. - * @param OperationRetVal result The result of the operation carried out by - * the controller's handling function. + * @param response The response object used to reply to the client. + * @param result The result of the operation carried out by the controller's + * handling function. */ - action(response: ServerResponse, result: OperationRetval) { + action(response: Response, result: OperationRetval) { if (result) { // Currently, the headers interface doesn't allow arbitrary string keys! const headers = (this.request.headers as any) || {}; diff --git a/examples/log-extension/src/providers/log-action.provider.ts b/examples/log-extension/src/providers/log-action.provider.ts index 9d477ba219a4..5e0b7ec72b42 100644 --- a/examples/log-extension/src/providers/log-action.provider.ts +++ b/examples/log-extension/src/providers/log-action.provider.ts @@ -5,7 +5,7 @@ import {inject, Provider, Constructor, Getter} from '@loopback/context'; import {CoreBindings} from '@loopback/core'; -import {OperationArgs, ParsedRequest} from '@loopback/rest'; +import {OperationArgs, Request} from '@loopback/rest'; import {getLogMetadata} from '../decorators'; import {EXAMPLE_LOG_BINDINGS, LOG_LEVEL} from '../keys'; import { @@ -35,7 +35,7 @@ export class LogActionProvider implements Provider { value(): LogFn { const fn = (( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, @@ -52,7 +52,7 @@ export class LogActionProvider implements Provider { } private async action( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, diff --git a/examples/log-extension/src/types.ts b/examples/log-extension/src/types.ts index 8e060d95e391..4db8be3559ee 100644 --- a/examples/log-extension/src/types.ts +++ b/examples/log-extension/src/types.ts @@ -5,14 +5,14 @@ // Types and interfaces exposed by the extension go here -import {ParsedRequest, OperationArgs} from '@loopback/rest'; +import {Request, OperationArgs} from '@loopback/rest'; /** * A function to perform REST req/res logging action */ export interface LogFn { ( - req: ParsedRequest, + req: Request, args: OperationArgs, // tslint:disable-next-line:no-any result: any, diff --git a/examples/log-extension/test/unit/providers/log-action.provider.unit.ts b/examples/log-extension/test/unit/providers/log-action.provider.unit.ts index eb98ee81858c..8afc25ac617b 100644 --- a/examples/log-extension/test/unit/providers/log-action.provider.unit.ts +++ b/examples/log-extension/test/unit/providers/log-action.provider.unit.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {sinon} from '@loopback/testlab'; -import {ParsedRequest} from '@loopback/rest'; +import {Request} from '@loopback/rest'; import { LogActionProvider, LogFn, @@ -21,7 +21,7 @@ import {logToMemory} from '../../in-memory-logger'; describe('LogActionProvider with in-memory logger', () => { let spy: sinon.SinonSpy; let logger: LogFn; - const req = {url: '/test'}; + const req = {url: '/test'}; beforeEach(() => { spy = createLogSpy(); @@ -60,7 +60,7 @@ describe('LogActionProvider with in-memory logger', () => { describe('LogActionProvider with default logger', () => { let stub: sinon.SinonSpy; let logger: LogFn; - const req = {url: '/test'}; + const req = {url: '/test'}; beforeEach(() => { stub = createConsoleStub(); diff --git a/packages/authentication/README.md b/packages/authentication/README.md index 9caf89c1939f..9eb0eb988dfd 100644 --- a/packages/authentication/README.md +++ b/packages/authentication/README.md @@ -113,11 +113,10 @@ import { InvokeMethod, Send, Reject, - ParsedRequest, + RequestContext, } from '@loopback/rest'; import {inject} from '@loopback/context'; import {AuthenticationBindings, AuthenticateFn} from '@loopback/authentication'; -import {ServerResponse} from 'http'; const SequenceActions = RestBindings.SequenceActions; diff --git a/packages/authentication/src/index.ts b/packages/authentication/src/index.ts index d1ce056046ff..9d67afd34c49 100644 --- a/packages/authentication/src/index.ts +++ b/packages/authentication/src/index.ts @@ -3,10 +3,8 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +export * from './types'; export * from './authentication.component'; export * from './decorators'; export * from './keys'; export * from './strategy-adapter'; - -// internals for tests -export * from './providers'; diff --git a/packages/authentication/src/keys.ts b/packages/authentication/src/keys.ts index 703080179a9c..7d0c5e885137 100644 --- a/packages/authentication/src/keys.ts +++ b/packages/authentication/src/keys.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Strategy} from 'passport'; -import {AuthenticateFn, UserProfile} from './providers/authentication.provider'; +import {AuthenticateFn, UserProfile} from './types'; import {AuthenticationMetadata} from './decorators/authenticate.decorator'; import {BindingKey, MetadataAccessor} from '@loopback/context'; @@ -37,20 +37,20 @@ export namespace AuthenticationBindings { * // ... other sequence action injections * ) {} * - * async handle(req: ParsedRequest, res: ServerResponse) { + * async handle(context: RequestContext) { * try { - * const route = this.findRoute(req); + * const {request, response} = context; + * const route = this.findRoute(request); * * // Authenticate - * await this.authenticateRequest(req); + * await this.authenticateRequest(request); * * // Authentication successful, proceed to invoke controller - * const args = await this.parseParams(req, route); + * const args = await this.parseParams(request, route); * const result = await this.invoke(route, args); - * this.send(res, result); + * this.send(response, result); * } catch (err) { - * this.reject(res, req, err); - * return; + * this.reject(context, err); * } * } * } diff --git a/packages/authentication/src/providers/authentication.provider.ts b/packages/authentication/src/providers/authentication.provider.ts index 90e45530a476..880f8a8f89dc 100644 --- a/packages/authentication/src/providers/authentication.provider.ts +++ b/packages/authentication/src/providers/authentication.provider.ts @@ -3,36 +3,12 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ParsedRequest} from '@loopback/rest'; -import {inject} from '@loopback/core'; -import {Provider, Getter, Setter} from '@loopback/context'; +import {Getter, Provider, Setter, inject} from '@loopback/context'; +import {Request} from '@loopback/rest'; import {Strategy} from 'passport'; -import {StrategyAdapter} from '../strategy-adapter'; import {AuthenticationBindings} from '../keys'; - -/** - * Passport monkey-patches Node.js' IncomingMessage prototype - * and adds extra methods like "login" and "isAuthenticated" - */ -export type PassportRequest = ParsedRequest & Express.Request; - -/** - * interface definition of a function which accepts a request - * and returns an authenticated user - */ -export interface AuthenticateFn { - (request: ParsedRequest): Promise; -} - -/** - * interface definition of a user profile - * http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - */ -export interface UserProfile { - id: string; - name?: string; - email?: string; -} +import {StrategyAdapter} from '../strategy-adapter'; +import {AuthenticateFn, UserProfile} from '../types'; /** * @description Provider of a function which authenticates @@ -63,9 +39,9 @@ export class AuthenticateActionProvider implements Provider { /** * The implementation of authenticate() sequence action. - * @param request Parsed Request + * @param request The incoming request provided by the REST layer */ - async action(request: ParsedRequest): Promise { + async action(request: Request): Promise { const strategy = await this.getStrategy(); if (!strategy) { // The invoked operation does not require authentication. diff --git a/packages/authentication/src/strategy-adapter.ts b/packages/authentication/src/strategy-adapter.ts index f07e4bb79021..1747db7cab24 100644 --- a/packages/authentication/src/strategy-adapter.ts +++ b/packages/authentication/src/strategy-adapter.ts @@ -3,59 +3,11 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {HttpErrors, ParsedRequest} from '@loopback/rest'; +import {HttpErrors, Request} from '@loopback/rest'; import {Strategy} from 'passport'; -import {UserProfile} from './providers/authentication.provider'; +import {UserProfile} from './types'; -const PassportRequestExtras: Express.Request = require('passport/lib/http/request'); - -/** - * Shimmed Request to satisfy express requirements of passport strategies. - */ -export class ShimRequest implements Express.Request { - headers: Object; - query: Object; - url: string; - path: string; - method: string; - - constructor(request: ParsedRequest) { - this.headers = request.headers; - this.query = request.query; - this.url = request.url; - this.path = request.path; - this.method = request.method; - } - - // tslint:disable:no-any - login(user: any, done: (err: any) => void): void; - login(user: any, options: any, done: (err: any) => void): void; - login(user: any, options: any, done?: any) { - PassportRequestExtras.login.apply(this, arguments); - } - - logIn(user: any, done: (err: any) => void): void; - logIn(user: any, options: any, done: (err: any) => void): void; - logIn(user: any, options: any, done?: any) { - PassportRequestExtras.logIn.apply(this, arguments); - } - - logout(): void { - PassportRequestExtras.logout.apply(this, arguments); - } - - logOut(): void { - PassportRequestExtras.logOut.apply(this, arguments); - } - - isAuthenticated(): boolean { - return PassportRequestExtras.isAuthenticated.apply(this, arguments); - } - isUnauthenticated(): boolean { - return PassportRequestExtras.isUnauthenticated.apply(this, arguments); - } - // tslint:enable:no-any -} +const passportRequestMixin = require('passport/lib/http/request'); /** * Adapter class to invoke passport-strategy @@ -77,11 +29,16 @@ export class StrategyAdapter { * 1. Create an instance of the strategy * 2. add success and failure state handlers * 3. authenticate using the strategy - * @param req {http.ServerRequest} The incoming request. + * @param request The incoming request. */ - authenticate(req: ParsedRequest) { - const shimReq = new ShimRequest(req); + authenticate(request: Request): Promise { return new Promise((resolve, reject) => { + // mix-in passport additions like req.logIn and req.logOut + for (const key in passportRequestMixin) { + // tslint:disable-next-line:no-any + (request as any)[key] = passportRequestMixin[key]; + } + // create a prototype chain of an instance of a passport strategy const strategy = Object.create(this.strategy); @@ -101,7 +58,7 @@ export class StrategyAdapter { }; // authenticate - strategy.authenticate(shimReq); + strategy.authenticate(request); }); } } diff --git a/packages/authentication/src/types.ts b/packages/authentication/src/types.ts new file mode 100644 index 000000000000..12a170ce3d29 --- /dev/null +++ b/packages/authentication/src/types.ts @@ -0,0 +1,24 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/authentication +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Request} from '@loopback/rest'; + +/** + * interface definition of a function which accepts a request + * and returns an authenticated user + */ +export interface AuthenticateFn { + (request: Request): Promise; +} + +/** + * interface definition of a user profile + * http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + */ +export interface UserProfile { + id: string; + name?: string; + email?: string; +} diff --git a/packages/authentication/test/unit/fixtures/mock-strategy.ts b/packages/authentication/test/unit/fixtures/mock-strategy.ts index 4726644f92b4..f315a81db13e 100644 --- a/packages/authentication/test/unit/fixtures/mock-strategy.ts +++ b/packages/authentication/test/unit/fixtures/mock-strategy.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Strategy, AuthenticateOptions} from 'passport'; -import {PassportRequest} from '../../..'; +import {Request} from 'express'; /** * Test fixture for a mock asynchronous passport-strategy @@ -21,7 +21,7 @@ export class MockStrategy extends Strategy { * authenticate() function similar to passport-strategy packages * @param req */ - async authenticate(req: Express.Request, options?: AuthenticateOptions) { + async authenticate(req: Request, options?: AuthenticateOptions) { await this.verify(req); } /** @@ -33,21 +33,18 @@ export class MockStrategy extends Strategy { * pass req.query.testState = 'fail' to mock failed authorization * pass req.query.testState = 'error' to mock unexpected error */ - async verify(request: Express.Request) { - // A workaround for buggy typings provided by @types/passport - const req = request as PassportRequest; - + async verify(request: Request) { if ( - req.headers && - req.headers.testState && - req.headers.testState === 'fail' + request.headers && + request.headers.testState && + request.headers.testState === 'fail' ) { this.returnUnauthorized('authorization failed'); return; } else if ( - req.headers && - req.headers.testState && - req.headers.testState === 'error' + request.headers && + request.headers.testState && + request.headers.testState === 'error' ) { this.returnError('unexpected error'); return; diff --git a/packages/authentication/test/unit/providers/auth-metadata.provider.unit.ts b/packages/authentication/test/unit/providers/auth-metadata.provider.unit.ts index 28adfc50ecda..5f82a0e59169 100644 --- a/packages/authentication/test/unit/providers/auth-metadata.provider.unit.ts +++ b/packages/authentication/test/unit/providers/auth-metadata.provider.unit.ts @@ -6,11 +6,8 @@ import {expect} from '@loopback/testlab'; import {CoreBindings} from '@loopback/core'; import {Context, Provider} from '@loopback/context'; -import { - AuthMetadataProvider, - AuthenticationMetadata, - authenticate, -} from '../../..'; +import {AuthenticationMetadata, authenticate} from '../../..'; +import {AuthMetadataProvider} from '../../../src/providers'; describe('AuthMetadataProvider', () => { let provider: Provider; diff --git a/packages/authentication/test/unit/providers/authentication.provider.unit.ts b/packages/authentication/test/unit/providers/authentication.provider.unit.ts index 64901c1f9715..0c727be83e7b 100644 --- a/packages/authentication/test/unit/providers/authentication.provider.unit.ts +++ b/packages/authentication/test/unit/providers/authentication.provider.unit.ts @@ -5,15 +5,11 @@ import {expect} from '@loopback/testlab'; import {Context, instantiateClass} from '@loopback/context'; -import {ParsedRequest} from '@loopback/rest'; -import { - AuthenticateActionProvider, - AuthenticateFn, - UserProfile, - AuthenticationBindings, -} from '../../..'; +import {Request} from '@loopback/rest'; +import {AuthenticateFn, UserProfile, AuthenticationBindings} from '../../..'; import {MockStrategy} from '../fixtures/mock-strategy'; import {Strategy} from 'passport'; +import {AuthenticateActionProvider} from '../../../src/providers'; describe('AuthenticateActionProvider', () => { describe('constructor()', () => { @@ -42,14 +38,14 @@ describe('AuthenticateActionProvider', () => { const authenticate: AuthenticateFn = await Promise.resolve( provider.value(), ); - const request = {}; + const request = {}; const user = await authenticate(request); expect(user).to.be.equal(mockUser); }); it('updates current user', async () => { const authenticate = await Promise.resolve(provider.value()); - const request = {}; + const request = {}; await authenticate(request); expect(currentUser).to.equal(mockUser); }); @@ -61,7 +57,7 @@ describe('AuthenticateActionProvider', () => { context .bind(AuthenticationBindings.AUTH_ACTION) .toProvider(AuthenticateActionProvider); - const request = {}; + const request = {}; const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); @@ -78,7 +74,7 @@ describe('AuthenticateActionProvider', () => { const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); - const request = {}; + const request = {}; let error; try { await authenticate(request); @@ -97,7 +93,7 @@ describe('AuthenticateActionProvider', () => { const authenticate = await context.get( AuthenticationBindings.AUTH_ACTION, ); - const request = {}; + const request = {}; request.headers = {testState: 'fail'}; let error; try { diff --git a/packages/authentication/test/unit/strategy-adapter.unit.ts b/packages/authentication/test/unit/strategy-adapter.unit.ts index 6cae799d1ace..796976204c7e 100644 --- a/packages/authentication/test/unit/strategy-adapter.unit.ts +++ b/packages/authentication/test/unit/strategy-adapter.unit.ts @@ -5,7 +5,7 @@ import {expect} from '@loopback/testlab'; import {StrategyAdapter, UserProfile} from '../..'; -import {ParsedRequest, HttpErrors} from '@loopback/rest'; +import {Request, HttpErrors} from '@loopback/rest'; import {MockStrategy} from './fixtures/mock-strategy'; import {AuthenticateOptions} from 'passport'; @@ -28,7 +28,7 @@ describe('Strategy Adapter', () => { } const strategy = new Strategy(); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; await adapter.authenticate(request); expect(calledFlag).to.be.true(); }); @@ -37,7 +37,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; const user: Object = await adapter.authenticate(request); expect(user).to.be.eql(mockUser); }); @@ -46,7 +46,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; request.headers = {testState: 'fail'}; let error; try { @@ -61,7 +61,7 @@ describe('Strategy Adapter', () => { const strategy = new MockStrategy(); strategy.setMockUser(mockUser); const adapter = new StrategyAdapter(strategy); - const request = {}; + const request = {}; request.headers = {testState: 'error'}; let error; try { diff --git a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs index 047d2f3af1ae..6336675b3062 100644 --- a/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs +++ b/packages/cli/generators/app/templates/src/controllers/ping.controller.ts.ejs @@ -1,4 +1,4 @@ -import {ParsedRequest, RestBindings} from '@loopback/rest'; +import {Request, RestBindings} from '@loopback/rest'; import {get} from '@loopback/openapi-v3'; import {inject} from '@loopback/context'; @@ -6,7 +6,7 @@ import {inject} from '@loopback/context'; * A simple controller to bounce back http requests */ export class PingController { - constructor(@inject(RestBindings.Http.REQUEST) private req: ParsedRequest) {} + constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {} // Map to `GET /ping` @get('/ping') diff --git a/packages/rest/package.json b/packages/rest/package.json index 08d480d5a077..204ba4da1937 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -28,10 +28,12 @@ "@loopback/openapi-v3": "^0.10.3", "@loopback/openapi-v3-types": "^0.7.3", "@types/cors": "^2.8.3", + "@types/express": "^4.11.1", "@types/http-errors": "^1.6.1", "body": "^5.1.0", "cors": "^2.8.4", "debug": "^3.1.0", + "express": "^4.16.3", "http-errors": "^1.6.3", "js-yaml": "^3.11.0", "lodash": "^4.17.5", diff --git a/packages/rest/src/http-handler.ts b/packages/rest/src/http-handler.ts index d90c600162fb..2fe59e3ad152 100644 --- a/packages/rest/src/http-handler.ts +++ b/packages/rest/src/http-handler.ts @@ -5,19 +5,17 @@ import {Context} from '@loopback/context'; import {PathObject, SchemasObject} from '@loopback/openapi-v3-types'; -import {ServerRequest, ServerResponse} from 'http'; import {ControllerSpec} from '@loopback/openapi-v3'; import {SequenceHandler} from './sequence'; import { RoutingTable, - parseRequestUrl, ResolvedRoute, RouteEntry, ControllerClass, ControllerFactory, } from './router/routing-table'; -import {ParsedRequest} from './types'; +import {Request, Response} from './types'; import {RestBindings} from './keys'; import {RequestContext} from './request-context'; @@ -26,10 +24,7 @@ export class HttpHandler { protected _routes: RoutingTable = new RoutingTable(); protected _apiDefinitions: SchemasObject; - public handleRequest: ( - request: ServerRequest, - response: ServerResponse, - ) => Promise; + public handleRequest: (request: Request, response: Response) => Promise; constructor(protected _rootContext: Context) { this.handleRequest = (req, res) => this._handleRequest(req, res); @@ -59,17 +54,16 @@ export class HttpHandler { return this._routes.describeApiPaths(); } - findRoute(request: ParsedRequest): ResolvedRoute { + findRoute(request: Request): ResolvedRoute { return this._routes.find(request); } protected async _handleRequest( - request: ServerRequest, - response: ServerResponse, + request: Request, + response: Response, ): Promise { - const parsedRequest: ParsedRequest = parseRequestUrl(request); const requestContext = new RequestContext( - parsedRequest, + request, response, this._rootContext, ); diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 80f06b45a196..e99c016069e1 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -3,9 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -// external dependencies -export {ServerRequest, ServerResponse} from 'http'; - export { RouteEntry, RoutingTable, @@ -13,7 +10,6 @@ export { ControllerRoute, ResolvedRoute, createResolvedRoute, - parseRequestUrl, ControllerClass, ControllerInstance, ControllerFactory, diff --git a/packages/rest/src/keys.ts b/packages/rest/src/keys.ts index db91b7e25437..8ebc0c8d7033 100644 --- a/packages/rest/src/keys.ts +++ b/packages/rest/src/keys.ts @@ -3,7 +3,6 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse} from 'http'; import {CoreBindings} from '@loopback/core'; import {BindingKey, Context} from '@loopback/context'; import {OpenApiSpec} from '@loopback/openapi-v3-types'; @@ -16,7 +15,8 @@ import { GetFromContext, InvokeMethod, LogError, - ParsedRequest, + Request, + Response, ParseParams, Reject, Send, @@ -116,15 +116,11 @@ export namespace RestBindings { /** * Binding key for setting and injecting the http request */ - export const REQUEST = BindingKey.create( - 'rest.http.request', - ); + export const REQUEST = BindingKey.create('rest.http.request'); /** * Binding key for setting and injecting the http response */ - export const RESPONSE = BindingKey.create( - 'rest.http.response', - ); + export const RESPONSE = BindingKey.create('rest.http.response'); /** * Binding key for setting and injecting the http request context */ diff --git a/packages/rest/src/parser.ts b/packages/rest/src/parser.ts index 8fbb88ec9f21..bb7da42c8f93 100644 --- a/packages/rest/src/parser.ts +++ b/packages/rest/src/parser.ts @@ -12,7 +12,7 @@ import { } from '@loopback/openapi-v3-types'; import {REQUEST_BODY_INDEX} from '@loopback/openapi-v3'; import {promisify} from 'util'; -import {OperationArgs, ParsedRequest, PathParameterValues} from './types'; +import {OperationArgs, Request, PathParameterValues} from './types'; import {ResolvedRoute} from './router/routing-table'; type HttpError = HttpErrors.HttpError; @@ -27,7 +27,7 @@ const parseJsonBody: (req: ServerRequest) => Promise = promisify( * Get the content-type header value from the request * @param req Http request */ -function getContentType(req: ServerRequest): string | undefined { +function getContentType(req: Request): string | undefined { const val = req.headers['content-type']; if (typeof val === 'string') { return val; @@ -47,7 +47,7 @@ function getContentType(req: ServerRequest): string | undefined { * @param pathParams Path parameters in incoming HTTP request */ export async function parseOperationArgs( - request: ParsedRequest, + request: Request, route: ResolvedRoute, ): Promise { const operationSpec = route.spec; @@ -58,7 +58,7 @@ export async function parseOperationArgs( async function loadRequestBodyIfNeeded( operationSpec: OperationObject, - request: ServerRequest, + request: Request, ): Promise { if (!operationSpec.requestBody) return Promise.resolve(); @@ -77,7 +77,7 @@ async function loadRequestBodyIfNeeded( function buildOperationArguments( operationSpec: OperationObject, - request: ParsedRequest, + request: Request, pathParams: PathParameterValues, body?: MaybeBody, ): OperationArgs { diff --git a/packages/rest/src/providers/find-route.provider.ts b/packages/rest/src/providers/find-route.provider.ts index fb3cdc235b5f..eece97274cc9 100644 --- a/packages/rest/src/providers/find-route.provider.ts +++ b/packages/rest/src/providers/find-route.provider.ts @@ -4,7 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Context, inject, Provider} from '@loopback/context'; -import {FindRoute, ParsedRequest} from '../types'; +import {FindRoute, Request} from '../types'; import {HttpHandler} from '../http-handler'; import {RestBindings} from '../keys'; import {ResolvedRoute} from '../router/routing-table'; @@ -19,7 +19,7 @@ export class FindRouteProvider implements Provider { return request => this.action(request); } - action(request: ParsedRequest): ResolvedRoute { + action(request: Request): ResolvedRoute { const found = this.handler.findRoute(request); found.updateBindings(this.context); return found; diff --git a/packages/rest/src/providers/log-error.provider.ts b/packages/rest/src/providers/log-error.provider.ts index 077befa700a8..b9a60144fb7b 100644 --- a/packages/rest/src/providers/log-error.provider.ts +++ b/packages/rest/src/providers/log-error.provider.ts @@ -4,15 +4,14 @@ // License text available at https://opensource.org/licenses/MIT import {Provider} from '@loopback/context'; -import {ServerRequest} from 'http'; -import {LogError} from '../types'; +import {LogError, Request} from '../types'; export class LogErrorProvider implements Provider { value(): LogError { return (err, statusCode, req) => this.action(err, statusCode, req); } - action(err: Error, statusCode: number, req: ServerRequest) { + action(err: Error, statusCode: number, req: Request) { if (statusCode < 500) { return; } diff --git a/packages/rest/src/request-context.ts b/packages/rest/src/request-context.ts index 38092754802a..a928b3793327 100644 --- a/packages/rest/src/request-context.ts +++ b/packages/rest/src/request-context.ts @@ -4,8 +4,7 @@ // License text available at https://opensource.org/licenses/MIT import {Context} from '@loopback/context'; -import {ServerResponse} from 'http'; -import {HandlerContext, ParsedRequest} from './types'; +import {HandlerContext, Request, Response} from './types'; import {RestBindings} from './keys'; /** @@ -14,8 +13,8 @@ import {RestBindings} from './keys'; */ export class RequestContext extends Context implements HandlerContext { constructor( - public readonly request: ParsedRequest, - public readonly response: ServerResponse, + public readonly request: Request, + public readonly response: Response, parent: Context, name?: string, ) { @@ -23,7 +22,7 @@ export class RequestContext extends Context implements HandlerContext { this._setupBindings(request, response); } - private _setupBindings(request: ParsedRequest, response: ServerResponse) { + private _setupBindings(request: Request, response: Response) { this.bind(RestBindings.Http.REQUEST) .to(request) .lock(); diff --git a/packages/rest/src/rest.server.ts b/packages/rest/src/rest.server.ts index 0e32318111f8..5ad5bfe00761 100644 --- a/packages/rest/src/rest.server.ts +++ b/packages/rest/src/rest.server.ts @@ -23,9 +23,18 @@ import {Application, CoreBindings, Server} from '@loopback/core'; import {getControllerSpec} from '@loopback/openapi-v3'; import {HttpHandler} from './http-handler'; import {DefaultSequence, SequenceHandler, SequenceFunction} from './sequence'; -import {FindRoute, InvokeMethod, Send, Reject, ParseParams} from './types'; +import { + FindRoute, + InvokeMethod, + Send, + Reject, + ParseParams, + Request, + Response, +} from './types'; import {RestBindings} from './keys'; -import {RequestContext} from '.'; +import {RequestContext} from './request-context'; +import * as express from 'express'; export type HttpRequestListener = ( req: ServerRequest, @@ -119,6 +128,8 @@ export class RestServer extends Context implements Server, HttpServerLike { } protected _httpServer: Http.Server; + protected _expressApp: express.Application; + /** * @memberof RestServer * Creates an instance of RestServer. @@ -152,22 +163,31 @@ export class RestServer extends Context implements Server, HttpServerLike { this.sequence(options.sequence); } - this.requestHandler = (req: ServerRequest, res: ServerResponse) => { - try { - this._handleHttpRequest(req, res, options!).catch(err => - this._onUnhandledError(req, res, err), - ); - } catch (err) { - this._onUnhandledError(req, res, err); - } - }; + this._setupRequestHandler(options); this.bind(RestBindings.HANDLER).toDynamicValue(() => this.httpHandler); } + protected _setupRequestHandler(options: RestServerConfig) { + this._expressApp = express(); + this.requestHandler = this._expressApp; + + // Mount our router & request handler + this._expressApp.use((req, res, next) => { + this._handleHttpRequest(req, res, options!).catch(next); + }); + + // Mount our error handler + this._expressApp.use( + (err: Error, req: Request, res: Response, next: Function) => { + this._onUnhandledError(req, res, err); + }, + ); + } + protected _handleHttpRequest( - request: ServerRequest, - response: ServerResponse, + request: Request, + response: Response, options: RestServerConfig, ) { // allow CORS support for all endpoints so that users @@ -182,11 +202,8 @@ export class RestServer extends Context implements Server, HttpServerLike { credentials: true, }; - // FIXME: `cors` expects Express Request/Response but the implementation - // at https://github.com/expressjs/cors/blob/master/lib/index.js only uses - // http.ServerRequest/ServerResponse - // tslint:disable-next-line:no-any - cors(corsOptions)(request as any, response as any, () => {}); + // TODO(bajtos) Register cors as a middleware in _setupRequestHandler + cors(corsOptions)(request, response, () => {}); if (request.method === 'OPTIONS') { return Promise.resolve(); } @@ -308,8 +325,8 @@ export class RestServer extends Context implements Server, HttpServerLike { } private async _serveOpenApiSpec( - request: ServerRequest, - response: ServerResponse, + request: Request, + response: Response, options?: OpenApiSpecOptions, ) { options = options || {version: '3.0.0', format: 'json'}; @@ -326,18 +343,15 @@ export class RestServer extends Context implements Server, HttpServerLike { } private async _redirectToSwaggerUI( - request: ServerRequest, - response: ServerResponse, + request: Request, + response: Response, options: RestServerConfig, ) { - response.statusCode = 308; const baseUrl = options.apiExplorerUrl || 'https://loopback.io/api-explorer'; - response.setHeader( - 'Location', - `${baseUrl}?url=http://${request.headers.host}/openapi.json`, - ); - response.end(); + const openApiUrl = `http://${request.headers.host}/openapi.json`; + const fullUrl = `${baseUrl}?url=${openApiUrl}`; + response.redirect(308, fullUrl); } /** @@ -602,11 +616,7 @@ export class RestServer extends Context implements Server, HttpServerLike { }); } - protected _onUnhandledError( - req: ServerRequest, - res: ServerResponse, - err: Error, - ) { + protected _onUnhandledError(req: Request, res: Response, err: Error) { if (!res.headersSent) { res.statusCode = 500; res.end(); diff --git a/packages/rest/src/router/routing-table.ts b/packages/rest/src/router/routing-table.ts index 3e68902b2209..68015d64cc30 100644 --- a/packages/rest/src/router/routing-table.ts +++ b/packages/rest/src/router/routing-table.ts @@ -16,11 +16,10 @@ import { instantiateClass, ValueOrPromise, } from '@loopback/context'; -import {ServerRequest} from 'http'; import * as HttpErrors from 'http-errors'; import { - ParsedRequest, + Request, PathParameterValues, OperationArgs, OperationRetval, @@ -29,7 +28,6 @@ import { import {ControllerSpec} from '@loopback/openapi-v3'; import * as assert from 'assert'; -import * as url from 'url'; const debug = require('debug')('loopback:core:routing-table'); // TODO(bajtos) Refactor this code to use Trie-based lookup, @@ -38,31 +36,6 @@ const debug = require('debug')('loopback:core:routing-table'); import * as pathToRegexp from 'path-to-regexp'; import {CoreBindings} from '@loopback/core'; -/** - * Parse the URL of the incoming request and set additional properties - * on this request object: - * - `path` - * - `query` - * - * @private - * @param request - */ -export function parseRequestUrl(request: ServerRequest): ParsedRequest { - // TODO(bajtos) The following parsing can be skipped when the router - // is mounted on an express app - const parsedRequest = request as ParsedRequest; - const parsedUrl = url.parse(parsedRequest.url, true); - parsedRequest.path = parsedUrl.pathname || '/'; - // parsedUrl.query cannot be a string as it is parsed with - // parseQueryString = true - if (parsedUrl.query != null && typeof parsedUrl.query !== 'string') { - parsedRequest.query = parsedUrl.query; - } else { - parsedRequest.query = {}; - } - return parsedRequest; -} - /** * A controller instance with open properties/methods */ @@ -172,7 +145,7 @@ export class RoutingTable { * Map a request to a route * @param request */ - find(request: ParsedRequest): ResolvedRoute { + find(request: Request): ResolvedRoute { for (const entry of this._routes) { const match = entry.match(request); if (match) return match; @@ -205,7 +178,7 @@ export interface RouteEntry { * Map an http request to a route * @param request */ - match(request: ParsedRequest): ResolvedRoute | undefined; + match(request: Request): ResolvedRoute | undefined; /** * Update bindings for the request context @@ -263,7 +236,7 @@ export abstract class BaseRoute implements RouteEntry { }); } - match(request: ParsedRequest): ResolvedRoute | undefined { + match(request: Request): ResolvedRoute | undefined { debug('trying endpoint', this); if (this.verb !== request.method!.toLowerCase()) { debug(' -> verb mismatch'); diff --git a/packages/rest/src/sequence.ts b/packages/rest/src/sequence.ts index 31de58f5c689..25a95d00f7ef 100644 --- a/packages/rest/src/sequence.ts +++ b/packages/rest/src/sequence.ts @@ -5,14 +5,7 @@ const debug = require('debug')('loopback:core:sequence'); import {inject} from '@loopback/context'; -import { - FindRoute, - InvokeMethod, - ParsedRequest, - Send, - Reject, - ParseParams, -} from './types'; +import {FindRoute, InvokeMethod, Send, Reject, ParseParams} from './types'; import {RestBindings} from './keys'; import {RequestContext} from './request-context'; diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 03430bfa7946..cebcbaabc649 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -4,40 +4,31 @@ // License text available at https://opensource.org/licenses/MIT import {Binding, BoundValue} from '@loopback/context'; -import {ServerRequest, ServerResponse} from 'http'; import {ResolvedRoute, RouteEntry} from './router/routing-table'; +import {Request, Response} from 'express'; -export interface ParsedRequest extends ServerRequest { - // see http://expressjs.com/en/4x/api.html#req.path - path: string; - // see http://expressjs.com/en/4x/api.html#req.query - query: {[key: string]: string | string[]}; - // see https://github.com/DefinitelyTyped/DefinitelyTyped/issues/15808 - url: string; - pathname: string; - method: string; -} +export {Request, Response}; /** * An object holding HTTP request, response and other data * needed to handle an incoming HTTP request. */ export interface HandlerContext { - readonly request: ParsedRequest; - readonly response: ServerResponse; + readonly request: Request; + readonly response: Response; } /** * Find a route matching the incoming request. * Throw an error when no route was found. */ -export type FindRoute = (request: ParsedRequest) => ResolvedRoute; +export type FindRoute = (request: Request) => ResolvedRoute; /** * */ export type ParseParams = ( - request: ParsedRequest, + request: Request, route: ResolvedRoute, ) => Promise; @@ -61,7 +52,7 @@ export type InvokeMethod = ( * @param response The response the response to send to. * @param result The operation result to send. */ -export type Send = (response: ServerResponse, result: OperationRetval) => void; +export type Send = (response: Response, result: OperationRetval) => void; /** * Reject the request with an error. @@ -82,7 +73,7 @@ export type Reject = (handlerContext: HandlerContext, err: Error) => void; export type LogError = ( err: Error, statusCode: number, - request: ServerRequest, + request: Request, ) => void; // tslint:disable:no-any diff --git a/packages/rest/src/writer.ts b/packages/rest/src/writer.ts index f1532d56af4f..dae47ee18e43 100644 --- a/packages/rest/src/writer.ts +++ b/packages/rest/src/writer.ts @@ -3,8 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse as Response} from 'http'; -import {OperationRetval} from './types'; +import {OperationRetval, Response} from './types'; import {HttpError} from 'http-errors'; import {Readable} from 'stream'; diff --git a/packages/rest/test/acceptance/routing/routing.acceptance.ts b/packages/rest/test/acceptance/routing/routing.acceptance.ts index b7be41381634..9116a4133108 100644 --- a/packages/rest/test/acceptance/routing/routing.acceptance.ts +++ b/packages/rest/test/acceptance/routing/routing.acceptance.ts @@ -4,8 +4,8 @@ // License text available at https://opensource.org/licenses/MIT import { - ServerRequest, - ServerResponse, + Request, + Response, Route, RestBindings, RestServer, @@ -312,8 +312,8 @@ describe('Routing', () => { @api(spec) class StatusController { constructor( - @inject(RestBindings.Http.REQUEST) private request: ServerRequest, - @inject(RestBindings.Http.RESPONSE) private response: ServerResponse, + @inject(RestBindings.Http.REQUEST) private request: Request, + @inject(RestBindings.Http.RESPONSE) private response: Response, ) {} async getStatus(): Promise { diff --git a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts index 157065d08984..42d32f95068a 100644 --- a/packages/rest/test/acceptance/sequence/sequence.acceptance.ts +++ b/packages/rest/test/acceptance/sequence/sequence.acceptance.ts @@ -4,7 +4,6 @@ // License text available at https://opensource.org/licenses/MIT import { - ParsedRequest, FindRoute, InvokeMethod, Send, diff --git a/packages/rest/test/integration/http-handler.integration.ts b/packages/rest/test/integration/http-handler.integration.ts index c03936de4b4c..050326fa7901 100644 --- a/packages/rest/test/integration/http-handler.integration.ts +++ b/packages/rest/test/integration/http-handler.integration.ts @@ -20,6 +20,7 @@ import * as HttpErrors from 'http-errors'; import {ParameterObject, RequestBodyObject} from '@loopback/openapi-v3-types'; import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder'; import {createUnexpectedHttpErrorLogger} from '../helpers'; +import * as express from 'express'; const SequenceActions = RestBindings.SequenceActions; @@ -458,7 +459,8 @@ describe('HttpHandler', () => { } function givenClient() { - client = createClientForHandler((req, res) => { + const app = express(); + app.use((req, res) => { handler.handleRequest(req, res).catch(err => { // This should never happen. If we ever get here, // then it means "handler.handlerRequest()" crashed unexpectedly. @@ -469,5 +471,6 @@ describe('HttpHandler', () => { res.end(); }); }); + client = createClientForHandler(app); } }); diff --git a/packages/rest/test/unit/parser.unit.ts b/packages/rest/test/unit/parser.unit.ts index a221a4dc08b4..35c709e7051e 100644 --- a/packages/rest/test/unit/parser.unit.ts +++ b/packages/rest/test/unit/parser.unit.ts @@ -3,20 +3,23 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - parseOperationArgs, - ParsedRequest, - parseRequestUrl, - PathParameterValues, - Route, - createResolvedRoute, -} from '../..'; -import {expect, ShotRequest, ShotRequestOptions} from '@loopback/testlab'; import { OperationObject, ParameterObject, RequestBodyObject, } from '@loopback/openapi-v3-types'; +import { + ShotRequestOptions, + expect, + stubExpressContext, +} from '@loopback/testlab'; +import { + PathParameterValues, + Request, + Route, + createResolvedRoute, + parseOperationArgs, +} from '../..'; describe('operationArgsParser', () => { it('parses path parameters', async () => { @@ -68,8 +71,8 @@ describe('operationArgsParser', () => { }; } - function givenRequest(options?: ShotRequestOptions): ParsedRequest { - return parseRequestUrl(new ShotRequest(options || {url: '/'})); + function givenRequest(options?: ShotRequestOptions): Request { + return stubExpressContext(options).request; } function givenResolvedRoute( diff --git a/packages/rest/test/unit/rest.component.unit.ts b/packages/rest/test/unit/rest.component.unit.ts index 2e22406243cf..b4872068a65e 100644 --- a/packages/rest/test/unit/rest.component.unit.ts +++ b/packages/rest/test/unit/rest.component.unit.ts @@ -3,23 +3,22 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {expect, ShotRequest} from '@loopback/testlab'; +import {BoundValue, Context, Provider, inject} from '@loopback/context'; import { Application, - ProviderMap, - CoreBindings, Component, + CoreBindings, + ProviderMap, } from '@loopback/core'; -import {inject, Provider, BoundValue, Context} from '@loopback/context'; - +import {expect, stubExpressContext} from '@loopback/testlab'; import { - RestComponent, - RestServer, - RestBindings, - RestComponentConfig, - ServerRequest, HttpHandler, LogError, + Request, + RestBindings, + RestComponent, + RestComponentConfig, + RestServer, } from '../..'; const SequenceActions = RestBindings.SequenceActions; @@ -68,9 +67,9 @@ describe('RestComponent', () => { } } - class CustomLogger implements Provider { + class CustomLogger implements Provider { value() { - return (err: Error, statusCode: number, request: ServerRequest) => { + return (err: Error, statusCode: number, request: Request) => { lastLog = `${request.url} ${statusCode} ${err.message}`; }; } @@ -80,7 +79,8 @@ describe('RestComponent', () => { app.component(CustomRestComponent); const server = await app.getServer(RestServer); const logError = await server.get(SequenceActions.LOG_ERROR); - logError(new Error('test-error'), 400, new ShotRequest({url: '/'})); + const expressContext = stubExpressContext({url: '/'}); + logError(new Error('test-error'), 400, expressContext.request); expect(lastLog).to.equal('/ 400 test-error'); }); diff --git a/packages/rest/test/unit/router/reject.provider.unit.ts b/packages/rest/test/unit/router/reject.provider.unit.ts index 11b3322dd655..d596fdc10310 100644 --- a/packages/rest/test/unit/router/reject.provider.unit.ts +++ b/packages/rest/test/unit/router/reject.provider.unit.ts @@ -4,33 +4,26 @@ // License text available at https://opensource.org/licenses/MIT import { - RejectProvider, - LogError, - ParsedRequest, - HandlerContext, -} from '../../..'; - -import { + ExpressContextStub, + SinonSpy, expect, - mockResponse, - ShotResponseMock, sinon, - SinonSpy, + stubExpressContext, } from '@loopback/testlab'; +import {LogError, RejectProvider} from '../../..'; describe('reject', () => { const noopLogger: LogError = () => {}; const testError = new Error('test error'); - let mock: ShotResponseMock; - let mockedContext: HandlerContext; + let contextStub: ExpressContextStub; - beforeEach(givenMockedResponse); + beforeEach(givenStubbedContext); it('returns HTTP response with status code 500 by default', async () => { const reject = new RejectProvider(noopLogger).value(); - reject(mockedContext, testError); - const result = await mock.result; + reject(contextStub, testError); + const result = await contextStub.result; expect(result).to.have.property('statusCode', 500); }); @@ -39,18 +32,13 @@ describe('reject', () => { const logger = sinon.spy() as LogError & SinonSpy; const reject = new RejectProvider(logger).value(); - reject(mockedContext, testError); - await mock.result; + reject(contextStub, testError); + await contextStub.result; - sinon.assert.calledWith(logger, testError, 500, mock.request); + sinon.assert.calledWith(logger, testError, 500, contextStub.request); }); - function givenMockedResponse() { - mock = mockResponse(); - mockedContext = { - // FIXME(bajtos) Remove this explicit cast once we switch to Express - request: mock.request as ParsedRequest, - response: mock.response, - }; + function givenStubbedContext() { + contextStub = stubExpressContext(); } }); diff --git a/packages/rest/test/unit/router/routing-table.unit.ts b/packages/rest/test/unit/router/routing-table.unit.ts index 4507dd055231..2d38f331a2af 100644 --- a/packages/rest/test/unit/router/routing-table.unit.ts +++ b/packages/rest/test/unit/router/routing-table.unit.ts @@ -3,15 +3,14 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - ParsedRequest, - parseRequestUrl, - RoutingTable, - ControllerRoute, -} from '../../..'; -import {getControllerSpec, param, get} from '@loopback/openapi-v3'; -import {expect, ShotRequestOptions, ShotRequest} from '@loopback/testlab'; import {anOpenApiSpec} from '@loopback/openapi-spec-builder'; +import {get, getControllerSpec, param} from '@loopback/openapi-v3'; +import { + ShotRequestOptions, + expect, + stubExpressContext, +} from '@loopback/testlab'; +import {ControllerRoute, Request, RoutingTable} from '../../..'; describe('RoutingTable', () => { it('joins basePath and path', () => { @@ -107,7 +106,7 @@ describe('RoutingTable', () => { expect(route.describe()).to.equal('TestController.greet'); }); - function givenRequest(options?: ShotRequestOptions): ParsedRequest { - return parseRequestUrl(new ShotRequest(options || {url: '/'})); + function givenRequest(options?: ShotRequestOptions): Request { + return stubExpressContext(options).request; } }); diff --git a/packages/rest/test/unit/writer.unit.ts b/packages/rest/test/unit/writer.unit.ts index 77d651789c8e..e1190f7a833b 100644 --- a/packages/rest/test/unit/writer.unit.ts +++ b/packages/rest/test/unit/writer.unit.ts @@ -3,13 +3,13 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {ServerResponse, writeResultToResponse} from '../..'; +import {Response, writeResultToResponse} from '../..'; import {Duplex} from 'stream'; -import {expect, mockResponse, ShotObservedResponse} from '@loopback/testlab'; +import {expect, ObservedResponse, stubExpressContext} from '@loopback/testlab'; describe('writer', () => { - let response: ServerResponse; - let observedResponse: Promise; + let response: Response; + let observedResponse: Promise; beforeEach(setupResponseMock); @@ -69,7 +69,7 @@ describe('writer', () => { }); function setupResponseMock() { - const responseMock = mockResponse(); + const responseMock = stubExpressContext(); response = responseMock.response; observedResponse = responseMock.result; diff --git a/packages/testlab/package.json b/packages/testlab/package.json index e9331d6eb35c..f03935381a06 100644 --- a/packages/testlab/package.json +++ b/packages/testlab/package.json @@ -20,10 +20,12 @@ "license": "MIT", "dependencies": { "@loopback/dist-util": "^0.3.1", + "@types/express": "^4.11.1", "@types/fs-extra": "^5.0.1", "@types/shot": "^3.4.0", "@types/sinon": "^4.3.0", "@types/supertest": "^2.0.4", + "express": "^4.16.3", "fs-extra": "^6.0.1", "shot": "^4.0.5", "should": "^13.2.1", diff --git a/packages/testlab/src/shot.ts b/packages/testlab/src/shot.ts index 1d4b10efbfae..6393f20e89b6 100644 --- a/packages/testlab/src/shot.ts +++ b/packages/testlab/src/shot.ts @@ -8,6 +8,8 @@ * https://github.com/hapijs/shot */ +// tslint:disable:no-any + import {ServerRequest, ServerResponse} from 'http'; import * as util from 'util'; @@ -17,18 +19,26 @@ import { inject, } from 'shot'; +import * as express from 'express'; + export {inject, ShotRequestOptions}; // tslint:disable-next-line:variable-name -export const ShotRequest: ShotRequestCtor = require('shot/lib/request'); +const ShotRequest: ShotRequestCtor = require('shot/lib/request'); +type ShotRequestCtor = new (options: ShotRequestOptions) => ServerRequest; + +export function stubServerRequest(options: ShotRequestOptions): ServerRequest { + const stub = new ShotRequest(options); + // Hacky workaround for Express, see + // https://github.com/expressjs/express/blob/4.16.3/lib/middleware/init.js + // https://github.com/hapijs/shot/issues/82#issuecomment-247943773 + // https://github.com/jfhbrook/pickleback + Object.assign(stub, ShotRequest.prototype); + return stub; +} // tslint:disable-next-line:variable-name -export const ShotResponse: ShotResponseCtor = require('shot/lib/response'); - -export type ShotRequestCtor = new ( - options: ShotRequestOptions, -) => ServerRequest; - +const ShotResponse: ShotResponseCtor = require('shot/lib/response'); export type ShotCallback = (response: ResponseObject) => void; export type ShotResponseCtor = new ( @@ -36,49 +46,124 @@ export type ShotResponseCtor = new ( onEnd: ShotCallback, ) => ServerResponse; -export type ShotObservedResponse = ResponseObject; +export function stubServerResponse( + request: ServerRequest, + onEnd: ShotCallback, +): ServerResponse { + const stub = new ShotResponse(request, onEnd); + // Hacky workaround for Express, see + // https://github.com/expressjs/express/blob/4.16.3/lib/middleware/init.js + // https://github.com/hapijs/shot/issues/82#issuecomment-247943773 + // https://github.com/jfhbrook/pickleback + Object.assign(stub, ShotResponse.prototype); + return stub; +} -export interface ShotResponseMock { +export type ObservedResponse = ResponseObject; + +export interface HandlerContextStub { request: ServerRequest; response: ServerResponse; - result: Promise; + result: Promise; } -export function mockResponse( +export function stubHandlerContext( requestOptions: ShotRequestOptions = {url: '/'}, -): ShotResponseMock { - const request = new ShotRequest(requestOptions); +): HandlerContextStub { + const request = stubServerRequest(requestOptions); let response: ServerResponse | undefined; - let result = new Promise(resolve => { + let result = new Promise(resolve => { response = new ShotResponse(request, resolve); }); + const context = {request, response: response!, result}; + defineCustomContextInspect(context, requestOptions); + return context; +} + +export interface ExpressContextStub extends HandlerContextStub { + app: express.Application; + request: express.Request; + response: express.Response; + result: Promise; +} + +export function stubExpressContext( + requestOptions: ShotRequestOptions = {url: '/'}, +): ExpressContextStub { + const app = express(); + + const request = new ShotRequest(requestOptions) as express.Request; + // mix in Express Request API + const RequestApi = (express as any).request; + for (const key of Object.getOwnPropertyNames(RequestApi)) { + Object.defineProperty( + request, + key, + Object.getOwnPropertyDescriptor(RequestApi, key)!, + ); + } + request.app = app; + request.originalUrl = request.url; + + let response: express.Response | undefined; + let result = new Promise(resolve => { + response = new ShotResponse(request, resolve) as express.Response; + // mix in Express Response API + Object.assign(response, (express as any).response); + const ResponseApi = (express as any).response; + for (const key of Object.getOwnPropertyNames(ResponseApi)) { + Object.defineProperty( + response, + key, + Object.getOwnPropertyDescriptor(ResponseApi, key)!, + ); + } + response.app = app; + (response as any).req = request; + (request as any).res = response; + }); + + const context = {app, request, response: response!, result}; + defineCustomContextInspect(context, requestOptions); + return context; +} + +function defineCustomContextInspect( + context: HandlerContextStub, + requestOptions: ShotRequestOptions, +) { // Setup custom inspect functions to make test error messages easier to read - const inspectOpts = (depth: number) => util.inspect(requestOptions, {depth}); + const inspectOpts = (depth: number, opts: any) => + util.inspect(requestOptions, opts); + defineCustomInspect( - request, - depth => `[ShotRequest with options ${inspectOpts(depth)}]`, + context.request, + (depth, opts) => `[RequestStub with options ${inspectOpts(depth, opts)}]`, ); defineCustomInspect( - response, - depth => `[ShotResponse for request with options ${inspectOpts(depth)}]`, + context.response, + (depth, opts) => + `[ResponseStub for request with options ${inspectOpts(depth, opts)}]`, ); - result = result.then(r => { + context.result = context.result.then(r => { defineCustomInspect( r, - depth => - `[ShotObservedResponse for request with options ${inspectOpts(depth)}]`, + (depth, opts) => + `[ObservedResponse for request with options ${inspectOpts( + depth, + opts, + )}]`, ); return r; }); - - return {request, response: response!, result}; } -// tslint:disable:no-any -function defineCustomInspect(obj: any, inspectFn: (depth: number) => {}) { - obj.inspect = obj.toString = inspectFn; +function defineCustomInspect( + obj: any, + inspectFn: (depth: number, opts: any) => {}, +) { + obj[util.inspect.custom] = inspectFn; } -// tslint:enable:no-any