UNPKG

6 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,2019. All Rights Reserved.
2// Node module: @loopback/testlab
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6/*
7 * HTTP Request/Response mocks
8 * https://github.com/hapijs/shot
9 */
10
11/* eslint-disable @typescript-eslint/no-explicit-any */
12
13import express from 'express';
14import {IncomingMessage, ServerResponse} from 'http';
15import {
16 Listener as ShotListener,
17 RequestOptions as ShotRequestOptions,
18 ResponseObject,
19} from 'shot'; // <-- workaround for missing type-defs for @hapi/shot
20import util from 'util';
21
22const inject: (
23 dispatchFunc: ShotListener,
24 options: ShotRequestOptions,
25) => Promise<ResponseObject> = require('@hapi/shot');
26// ^^ workaround for missing type-defs for @hapi/shot
27
28export {inject, ShotRequestOptions};
29
30const ShotRequest: ShotRequestCtor = require('@hapi/shot/lib/request');
31type ShotRequestCtor = new (options: ShotRequestOptions) => IncomingMessage;
32
33export function stubServerRequest(
34 options: ShotRequestOptions,
35): IncomingMessage {
36 const stub = new ShotRequest(options);
37 // Hacky workaround for Express, see
38 // https://github.com/expressjs/express/blob/4.16.3/lib/middleware/init.js
39 // https://github.com/hapijs/shot/issues/82#issuecomment-247943773
40 // https://github.com/jfhbrook/pickleback
41 Object.assign(stub, ShotRequest.prototype);
42 return stub;
43}
44
45const ShotResponse: ShotResponseCtor = require('@hapi/shot/lib/response');
46export type ShotCallback = (response: ResponseObject) => void;
47
48export type ShotResponseCtor = new (
49 request: IncomingMessage,
50 onEnd: ShotCallback,
51) => ServerResponse;
52
53export function stubServerResponse(
54 request: IncomingMessage,
55 onEnd: ShotCallback,
56): ServerResponse {
57 const stub = new ShotResponse(request, onEnd);
58 // Hacky workaround for Express, see
59 // https://github.com/expressjs/express/blob/4.16.3/lib/middleware/init.js
60 // https://github.com/hapijs/shot/issues/82#issuecomment-247943773
61 // https://github.com/jfhbrook/pickleback
62 Object.assign(stub, ShotResponse.prototype);
63 return stub;
64}
65
66export type ObservedResponse = ResponseObject;
67
68export interface HandlerContextStub {
69 request: IncomingMessage;
70 response: ServerResponse;
71 result: Promise<ObservedResponse>;
72}
73
74export function stubHandlerContext(
75 requestOptions: ShotRequestOptions = {url: '/'},
76): HandlerContextStub {
77 const request = stubServerRequest(requestOptions);
78 let response: ServerResponse | undefined;
79 const result = new Promise<ObservedResponse>(resolve => {
80 response = new ShotResponse(request, resolve);
81 });
82
83 const context = {request, response: response!, result};
84 defineCustomContextInspect(context, requestOptions);
85 return context;
86}
87
88export interface ExpressContextStub extends HandlerContextStub {
89 app: express.Application;
90 request: express.Request;
91 response: express.Response;
92 result: Promise<ObservedResponse>;
93}
94
95export function stubExpressContext(
96 requestOptions: ShotRequestOptions = {url: '/'},
97): ExpressContextStub {
98 const app = express();
99
100 const request = new ShotRequest(requestOptions) as express.Request;
101 // mix in Express Request API
102 const RequestApi = (express as any).request;
103 for (const key of Object.getOwnPropertyNames(RequestApi)) {
104 Object.defineProperty(
105 request,
106 key,
107 Object.getOwnPropertyDescriptor(RequestApi, key)!,
108 );
109 }
110 request.app = app;
111 request.originalUrl = request.url;
112 parseQuery(request);
113
114 let response: express.Response | undefined;
115 const result = new Promise<ObservedResponse>(resolve => {
116 response = new ShotResponse(request, resolve) as express.Response;
117 // mix in Express Response API
118 Object.assign(response, (express as any).response);
119 const ResponseApi = (express as any).response;
120 for (const key of Object.getOwnPropertyNames(ResponseApi)) {
121 Object.defineProperty(
122 response,
123 key,
124 Object.getOwnPropertyDescriptor(ResponseApi, key)!,
125 );
126 }
127 response.app = app;
128 (response as any).req = request;
129 (request as any).res = response;
130 });
131
132 const context = {app, request, response: response!, result};
133 defineCustomContextInspect(context, requestOptions);
134 return context;
135}
136
137/**
138 * Use `express.query` to parse the query string into `request.query` object
139 * @param request - Express http request object
140 */
141function parseQuery(request: express.Request) {
142 // Use `express.query` to parse the query string
143 // See https://github.com/expressjs/express/blob/master/lib/express.js#L79
144 // See https://github.com/expressjs/express/blob/master/lib/middleware/query.js
145 (express as any).query()(request, {}, () => {});
146}
147
148function defineCustomContextInspect(
149 context: HandlerContextStub,
150 requestOptions: ShotRequestOptions,
151) {
152 // Setup custom inspect functions to make test error messages easier to read
153 const inspectOpts = (depth: number, opts: any) =>
154 util.inspect(requestOptions, opts);
155
156 defineCustomInspect(
157 context.request,
158 (depth, opts) => `[RequestStub with options ${inspectOpts(depth, opts)}]`,
159 );
160
161 defineCustomInspect(
162 context.response,
163 (depth, opts) =>
164 `[ResponseStub for request with options ${inspectOpts(depth, opts)}]`,
165 );
166
167 context.result = context.result.then(r => {
168 defineCustomInspect(
169 r,
170 (depth, opts) =>
171 `[ObservedResponse for request with options ${inspectOpts(
172 depth,
173 opts,
174 )}]`,
175 );
176 return r;
177 });
178}
179
180// @types/node@v10.17.29 seems to miss the type definition of `util.inspect.custom`
181// error TS2339: Property 'custom' does not exist on type 'typeof inspect'.
182// Use a workaround for now to access the `custom` symbol for now.
183// https://nodejs.org/api/util.html#util_util_inspect_custom
184const custom = Symbol.for('nodejs.util.inspect.custom');
185
186function defineCustomInspect(
187 obj: any,
188 inspectFn: (depth: number, opts: any) => {},
189) {
190 obj[custom] = inspectFn;
191}