1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | const { inspect } = require('util');
|
8 | const { STATUS_CODES } = require('http');
|
9 | const { Server } = require('https');
|
10 | const { deepStrictEqual } = require('assert');
|
11 | const { Request } = require('superagent');
|
12 |
|
13 |
|
14 |
|
15 | class Test extends Request {
|
16 | |
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 | constructor (app, method, path) {
|
26 | super(method.toUpperCase(), path);
|
27 |
|
28 | this.redirects(0);
|
29 | this.buffer();
|
30 | this.app = app;
|
31 | this._asserts = [];
|
32 | this.url = typeof app === 'string'
|
33 | ? app + path
|
34 | : this.serverAddress(app, path);
|
35 | }
|
36 |
|
37 | |
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | serverAddress(app, path) {
|
46 | const addr = app.address();
|
47 |
|
48 | if (!addr) this._server = app.listen(0);
|
49 | const port = app.address().port;
|
50 | const protocol = app instanceof Server ? 'https' : 'http';
|
51 | return protocol + '://127.0.0.1:' + port + path;
|
52 | }
|
53 |
|
54 | |
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 | expect(a, b, c) {
|
72 |
|
73 | if (typeof a === 'function') {
|
74 | this._asserts.push(wrapAssertFn(a));
|
75 | return this;
|
76 | }
|
77 | if (typeof b === 'function') this.end(b);
|
78 | if (typeof c === 'function') this.end(c);
|
79 |
|
80 |
|
81 | if (typeof a === 'number') {
|
82 | this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a)));
|
83 |
|
84 | if (typeof b !== 'function' && arguments.length > 1) {
|
85 | this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b)));
|
86 | }
|
87 | return this;
|
88 | }
|
89 |
|
90 |
|
91 | if (Array.isArray(a) && a.length > 0 && a.every(val => typeof val === 'number')) {
|
92 | this._asserts.push(wrapAssertFn(this._assertStatusArray.bind(this, a)));
|
93 | return this;
|
94 | }
|
95 |
|
96 |
|
97 | if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) {
|
98 | this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: '' + a, value: b })));
|
99 | return this;
|
100 | }
|
101 |
|
102 |
|
103 | this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a)));
|
104 |
|
105 | return this;
|
106 | }
|
107 |
|
108 | |
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | end(fn) {
|
116 | const server = this._server;
|
117 |
|
118 | super.end((err, res) => {
|
119 | const localAssert = () => {
|
120 | this.assert(err, res, fn);
|
121 | };
|
122 |
|
123 | if (server && server._handle) return server.close(localAssert);
|
124 |
|
125 | localAssert();
|
126 | });
|
127 |
|
128 | return this;
|
129 | }
|
130 |
|
131 | |
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | assert(resError, res, fn) {
|
140 | let errorObj;
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 | const sysErrors = {
|
147 | ECONNREFUSED: 'Connection refused',
|
148 | ECONNRESET: 'Connection reset by peer',
|
149 | EPIPE: 'Broken pipe',
|
150 | ETIMEDOUT: 'Operation timed out'
|
151 | };
|
152 |
|
153 | if (!res && resError) {
|
154 | if (resError instanceof Error && resError.syscall === 'connect'
|
155 | && Object.getOwnPropertyNames(sysErrors).indexOf(resError.code) >= 0) {
|
156 | errorObj = new Error(resError.code + ': ' + sysErrors[resError.code]);
|
157 | } else {
|
158 | errorObj = resError;
|
159 | }
|
160 | }
|
161 |
|
162 |
|
163 | for (let i = 0; i < this._asserts.length && !errorObj; i += 1) {
|
164 | errorObj = this._assertFunction(this._asserts[i], res);
|
165 | }
|
166 |
|
167 |
|
168 | if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) {
|
169 | errorObj = resError;
|
170 | }
|
171 |
|
172 | fn.call(this, errorObj || null, res);
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | _assertBody(body, res) {
|
184 | const isRegexp = body instanceof RegExp;
|
185 |
|
186 |
|
187 | if (typeof body === 'object' && !isRegexp) {
|
188 | try {
|
189 | deepStrictEqual(body, res.body);
|
190 | } catch (err) {
|
191 | const a = inspect(body);
|
192 | const b = inspect(res.body);
|
193 | return error('expected ' + a + ' response body, got ' + b, body, res.body);
|
194 | }
|
195 | } else if (body !== res.text) {
|
196 |
|
197 | const a = inspect(body);
|
198 | const b = inspect(res.text);
|
199 |
|
200 |
|
201 | if (isRegexp) {
|
202 | if (!body.test(res.text)) {
|
203 | return error('expected body ' + b + ' to match ' + body, body, res.body);
|
204 | }
|
205 | } else {
|
206 | return error('expected ' + a + ' response body, got ' + b, body, res.body);
|
207 | }
|
208 | }
|
209 | }
|
210 |
|
211 | |
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 | _assertHeader(header, res) {
|
220 | const field = header.name;
|
221 | const actual = res.header[field.toLowerCase()];
|
222 | const fieldExpected = header.value;
|
223 |
|
224 | if (typeof actual === 'undefined') return new Error('expected "' + field + '" header field');
|
225 |
|
226 | if ((Array.isArray(actual) && actual.toString() === fieldExpected)
|
227 | || fieldExpected === actual) {
|
228 | return;
|
229 | }
|
230 | if (fieldExpected instanceof RegExp) {
|
231 | if (!fieldExpected.test(actual)) {
|
232 | return new Error('expected "' + field + '" matching '
|
233 | + fieldExpected + ', got "' + actual + '"');
|
234 | }
|
235 | } else {
|
236 | return new Error('expected "' + field + '" of "' + fieldExpected + '", got "' + actual + '"');
|
237 | }
|
238 | }
|
239 |
|
240 | |
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | _assertStatus(status, res) {
|
249 | if (res.status !== status) {
|
250 | const a = STATUS_CODES[status];
|
251 | const b = STATUS_CODES[res.status];
|
252 | return new Error('expected ' + status + ' "' + a + '", got ' + res.status + ' "' + b + '"');
|
253 | }
|
254 | }
|
255 |
|
256 | |
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | _assertStatusArray(statusArray, res) {
|
265 | if (!statusArray.includes(res.status)) {
|
266 | const b = STATUS_CODES[res.status];
|
267 | const expectedList = statusArray.join(', ');
|
268 | return new Error(
|
269 | 'expected one of "' + expectedList + '", got ' + res.status + ' "' + b + '"'
|
270 | );
|
271 | }
|
272 | }
|
273 |
|
274 | |
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 | _assertFunction(fn, res) {
|
283 | let err;
|
284 | try {
|
285 | err = fn(res);
|
286 | } catch (e) {
|
287 | err = e;
|
288 | }
|
289 | if (err instanceof Error) return err;
|
290 | }
|
291 | }
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 | function wrapAssertFn(assertFn) {
|
302 | const savedStack = new Error().stack.split('\n').slice(3);
|
303 |
|
304 | return function(res) {
|
305 | let badStack;
|
306 | const err = assertFn(res);
|
307 | if (err instanceof Error && err.stack) {
|
308 | badStack = err.stack.replace(err.message, '').split('\n').slice(1);
|
309 | err.stack = [err.toString()]
|
310 | .concat(savedStack)
|
311 | .concat('----')
|
312 | .concat(badStack)
|
313 | .join('\n');
|
314 | }
|
315 | return err;
|
316 | };
|
317 | }
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | function error(msg, expected, actual) {
|
330 | const err = new Error(msg);
|
331 | err.expected = expected;
|
332 | err.actual = actual;
|
333 | err.showDiff = true;
|
334 | return err;
|
335 | }
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | module.exports = Test;
|