From d94ffbc9be6100076e9a8437836e5c57fd92ca80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 4 Jan 2022 14:45:26 +0100 Subject: [PATCH] refactor: Convert test to a class --- lib/test.js | 568 ++++++++++++++++++++++++++-------------------------- 1 file changed, 279 insertions(+), 289 deletions(-) diff --git a/lib/test.js b/lib/test.js index 7800abb..3414b49 100644 --- a/lib/test.js +++ b/lib/test.js @@ -11,336 +11,320 @@ var https = require('https'); var assert = require('assert'); var Request = request.Request; -/** - * Expose `Test`. - */ - -module.exports = Test; - -/** - * Initialize a new `Test` with the given `app`, - * request `method` and `path`. - * - * @param {Server} app - * @param {String} method - * @param {String} path - * @api public - */ - -function Test(app, method, path) { - Request.call(this, method.toUpperCase(), path); - this.redirects(0); - this.buffer(); - this.app = app; - this._asserts = []; - this.url = typeof app === 'string' - ? app + path - : this.serverAddress(app, path); -} - -/** - * Inherits from `Request.prototype`. - */ - -Object.setPrototypeOf(Test.prototype, Request.prototype); - -/** - * Returns a URL, extracted from a server. - * - * @param {Server} app - * @param {String} path - * @returns {String} URL address - * @api private - */ - -Test.prototype.serverAddress = function(app, path) { - var addr = app.address(); - var port; - var protocol; - - if (!addr) this._server = app.listen(0); - port = app.address().port; - protocol = app instanceof https.Server ? 'https' : 'http'; - return protocol + '://127.0.0.1:' + port + path; -}; - -/** - * Wraps an assert function into another. - * The wrapper function edit the stack trace of any assertion error, prepending a more useful stack to it. - * - * @param {Function} assertFn - * @returns {Function} wrapped assert function - */ +class Test extends Request { + /** + * Initialize a new `Test` with the given `app`, + * request `method` and `path`. + * + * @param {Server} app + * @param {String} method + * @param {String} path + * @api public + */ + constructor (app, method, path) { + super(method.toUpperCase(), path); + + this.redirects(0); + this.buffer(); + this.app = app; + this._asserts = []; + this.url = typeof app === 'string' + ? app + path + : this.serverAddress(app, path); + } -function wrapAssertFn(assertFn) { - var savedStack = new Error().stack.split('\n').slice(3); + /** + * Returns a URL, extracted from a server. + * + * @param {Server} app + * @param {String} path + * @returns {String} URL address + * @api private + */ + serverAddress(app, path) { + var addr = app.address(); + var port; + var protocol; + + if (!addr) this._server = app.listen(0); + port = app.address().port; + protocol = app instanceof https.Server ? 'https' : 'http'; + return protocol + '://127.0.0.1:' + port + path; + } - return function(res) { - var badStack; - var err = assertFn(res); - if (err instanceof Error && err.stack) { - badStack = err.stack.replace(err.message, '').split('\n').slice(1); - err.stack = [err.toString()] - .concat(savedStack) - .concat('----') - .concat(badStack) - .join('\n'); + /** + * Expectations: + * + * .expect(200) + * .expect(200, fn) + * .expect(200, body) + * .expect('Some body') + * .expect('Some body', fn) + * .expect(['json array body', { key: 'val' }]) + * .expect('Content-Type', 'application/json') + * .expect('Content-Type', 'application/json', fn) + * .expect(fn) + * .expect([200, 404]) + * + * @return {Test} + * @api public + */ + expect(a, b, c) { + // callback + if (typeof a === 'function') { + this._asserts.push(wrapAssertFn(a)); + return this; + } + if (typeof b === 'function') this.end(b); + if (typeof c === 'function') this.end(c); + + // status + if (typeof a === 'number') { + this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a))); + // body + if (typeof b !== 'function' && arguments.length > 1) { + this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b))); + } + return this; } - return err; - }; -} -/** - * Expectations: - * - * .expect(200) - * .expect(200, fn) - * .expect(200, body) - * .expect('Some body') - * .expect('Some body', fn) - * .expect(['json array body', { key: 'val' }]) - * .expect('Content-Type', 'application/json') - * .expect('Content-Type', 'application/json', fn) - * .expect(fn) - * .expect([200, 404]) - * - * @return {Test} - * @api public - */ + // multiple statuses + if (Array.isArray(a) && a.length > 0 && a.every(val => typeof val === 'number')) { + this._asserts.push(wrapAssertFn(this._assertStatusArray.bind(this, a))); + return this; + } -Test.prototype.expect = function(a, b, c) { - // callback - if (typeof a === 'function') { - this._asserts.push(wrapAssertFn(a)); - return this; - } - if (typeof b === 'function') this.end(b); - if (typeof c === 'function') this.end(c); + // header field + if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) { + this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: '' + a, value: b }))); + return this; + } - // status - if (typeof a === 'number') { - this._asserts.push(wrapAssertFn(this._assertStatus.bind(this, a))); // body - if (typeof b !== 'function' && arguments.length > 1) { - this._asserts.push(wrapAssertFn(this._assertBody.bind(this, b))); - } - return this; - } + this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a))); - // multiple statuses - if (Array.isArray(a) && a.length > 0 && a.every(val => typeof val === 'number')) { - this._asserts.push(wrapAssertFn(this._assertStatusArray.bind(this, a))); return this; } - // header field - if (typeof b === 'string' || typeof b === 'number' || b instanceof RegExp) { - this._asserts.push(wrapAssertFn(this._assertHeader.bind(this, { name: '' + a, value: b }))); + /** + * Defer invoking superagent's `.end()` until + * the server is listening. + * + * @param {Function} fn + * @api public + */ + end(fn) { + var self = this; + var server = this._server; + var end = Request.prototype.end; + + end.call(this, function (err, res) { + if (server && server._handle) return server.close(localAssert); + + localAssert(); + + function localAssert() { + self.assert(err, res, fn); + } + }); + return this; } - // body - this._asserts.push(wrapAssertFn(this._assertBody.bind(this, a))); - - return this; -}; - -/** - * Defer invoking superagent's `.end()` until - * the server is listening. - * - * @param {Function} fn - * @api public - */ - -Test.prototype.end = function(fn) { - var self = this; - var server = this._server; - var end = Request.prototype.end; - - end.call(this, function(err, res) { - if (server && server._handle) return server.close(localAssert); - - localAssert(); - - function localAssert() { - self.assert(err, res, fn); + /** + * Perform assertions and invoke `fn(err, res)`. + * + * @param {?Error} resError + * @param {Response} res + * @param {Function} fn + * @api private + */ + assert(resError, res, fn) { + var errorObj; + var i; + + // check for unexpected network errors or server not running/reachable errors + // when there is no response and superagent sends back a System Error + // do not check further for other asserts, if any, in such case + // https://nodejs.org/api/errors.html#errors_common_system_errors + var sysErrors = { + ECONNREFUSED: 'Connection refused', + ECONNRESET: 'Connection reset by peer', + EPIPE: 'Broken pipe', + ETIMEDOUT: 'Operation timed out' + }; + + if (!res && resError) { + if (resError instanceof Error && resError.syscall === 'connect' + && Object.getOwnPropertyNames(sysErrors).indexOf(resError.code) >= 0) { + errorObj = new Error(resError.code + ': ' + sysErrors[resError.code]); + } else { + errorObj = resError; + } } - }); - return this; -}; - -/** - * Perform assertions and invoke `fn(err, res)`. - * - * @param {?Error} resError - * @param {Response} res - * @param {Function} fn - * @api private - */ - -Test.prototype.assert = function(resError, res, fn) { - var errorObj; - var i; - - // check for unexpected network errors or server not running/reachable errors - // when there is no response and superagent sends back a System Error - // do not check further for other asserts, if any, in such case - // https://nodejs.org/api/errors.html#errors_common_system_errors - var sysErrors = { - ECONNREFUSED: 'Connection refused', - ECONNRESET: 'Connection reset by peer', - EPIPE: 'Broken pipe', - ETIMEDOUT: 'Operation timed out' - }; + // asserts + for (i = 0; i < this._asserts.length && !errorObj; i += 1) { + errorObj = this._assertFunction(this._asserts[i], res); + } - if (!res && resError) { - if (resError instanceof Error && resError.syscall === 'connect' - && Object.getOwnPropertyNames(sysErrors).indexOf(resError.code) >= 0) { - errorObj = new Error(resError.code + ': ' + sysErrors[resError.code]); - } else { + // set unexpected superagent error if no other error has occurred. + if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) { errorObj = resError; } - } - // asserts - for (i = 0; i < this._asserts.length && !errorObj; i += 1) { - errorObj = this._assertFunction(this._asserts[i], res); + fn.call(this, errorObj || null, res); } - // set unexpected superagent error if no other error has occurred. - if (!errorObj && resError instanceof Error && (!res || resError.status !== res.status)) { - errorObj = resError; + /** + * Perform assertions on a response body and return an Error upon failure. + * + * @param {Mixed} body + * @param {Response} res + * @return {?Error} + * @api private + */// eslint-disable-next-line class-methods-use-this + _assertBody(body, res) { + var isregexp = body instanceof RegExp; + var a; + var b; + + // parsed + if (typeof body === 'object' && !isregexp) { + try { + assert.deepStrictEqual(body, res.body); + } catch (err) { + a = util.inspect(body); + b = util.inspect(res.body); + return error('expected ' + a + ' response body, got ' + b, body, res.body); + } + } else if (body !== res.text) { + // string + a = util.inspect(body); + b = util.inspect(res.text); + + // regexp + if (isregexp) { + if (!body.test(res.text)) { + return error('expected body ' + b + ' to match ' + body, body, res.body); + } + } else { + return error('expected ' + a + ' response body, got ' + b, body, res.body); + } + } } - fn.call(this, errorObj || null, res); -}; - -/** - * Perform assertions on a response body and return an Error upon failure. - * - * @param {Mixed} body - * @param {Response} res - * @return {?Error} - * @api private - */ - -Test.prototype._assertBody = function(body, res) { - var isregexp = body instanceof RegExp; - var a; - var b; - - // parsed - if (typeof body === 'object' && !isregexp) { - try { - assert.deepStrictEqual(body, res.body); - } catch (err) { - a = util.inspect(body); - b = util.inspect(res.body); - return error('expected ' + a + ' response body, got ' + b, body, res.body); + /** + * Perform assertions on a response header and return an Error upon failure. + * + * @param {Object} header + * @param {Response} res + * @return {?Error} + * @api private + */// eslint-disable-next-line class-methods-use-this + _assertHeader(header, res) { + var field = header.name; + var actual = res.header[field.toLowerCase()]; + var fieldExpected = header.value; + + if (typeof actual === 'undefined') return new Error('expected "' + field + '" header field'); + // This check handles header values that may be a String or single element Array + if ((Array.isArray(actual) && actual.toString() === fieldExpected) + || fieldExpected === actual) { + return; } - } else if (body !== res.text) { - // string - a = util.inspect(body); - b = util.inspect(res.text); - - // regexp - if (isregexp) { - if (!body.test(res.text)) { - return error('expected body ' + b + ' to match ' + body, body, res.body); + if (fieldExpected instanceof RegExp) { + if (!fieldExpected.test(actual)) { + return new Error('expected "' + field + '" matching ' + + fieldExpected + ', got "' + actual + '"'); } } else { - return error('expected ' + a + ' response body, got ' + b, body, res.body); + return new Error('expected "' + field + '" of "' + fieldExpected + '", got "' + actual + '"'); } } -}; - -/** - * Perform assertions on a response header and return an Error upon failure. - * - * @param {Object} header - * @param {Response} res - * @return {?Error} - * @api private - */ - -Test.prototype._assertHeader = function(header, res) { - var field = header.name; - var actual = res.header[field.toLowerCase()]; - var fieldExpected = header.value; - if (typeof actual === 'undefined') return new Error('expected "' + field + '" header field'); - // This check handles header values that may be a String or single element Array - if ((Array.isArray(actual) && actual.toString() === fieldExpected) - || fieldExpected === actual) { - return; - } - if (fieldExpected instanceof RegExp) { - if (!fieldExpected.test(actual)) { - return new Error('expected "' + field + '" matching ' - + fieldExpected + ', got "' + actual + '"'); + /** + * Perform assertions on the response status and return an Error upon failure. + * + * @param {Number} status + * @param {Response} res + * @return {?Error} + * @api private + */// eslint-disable-next-line class-methods-use-this + _assertStatus(status, res) { + var a; + var b; + if (res.status !== status) { + a = http.STATUS_CODES[status]; + b = http.STATUS_CODES[res.status]; + return new Error('expected ' + status + ' "' + a + '", got ' + res.status + ' "' + b + '"'); } - } else { - return new Error('expected "' + field + '" of "' + fieldExpected + '", got "' + actual + '"'); } -}; -/** - * Perform assertions on the response status and return an Error upon failure. - * - * @param {Number} status - * @param {Response} res - * @return {?Error} - * @api private - */ + /** + * Perform assertions on the response status and return an Error upon failure. + * + * @param {Array} statusArray + * @param {Response} res + * @return {?Error} + * @api private + */// eslint-disable-next-line class-methods-use-this + _assertStatusArray(statusArray, res) { + var b; + var expectedList; + if (!statusArray.includes(res.status)) { + b = http.STATUS_CODES[res.status]; + expectedList = statusArray.join(', '); + return new Error( + 'expected one of "' + expectedList + '", got ' + res.status + ' "' + b + '"' + ); + } + } -Test.prototype._assertStatus = function(status, res) { - var a; - var b; - if (res.status !== status) { - a = http.STATUS_CODES[status]; - b = http.STATUS_CODES[res.status]; - return new Error('expected ' + status + ' "' + a + '", got ' + res.status + ' "' + b + '"'); + /** + * Performs an assertion by calling a function and return an Error upon failure. + * + * @param {Function} fn + * @param {Response} res + * @return {?Error} + * @api private + */// eslint-disable-next-line class-methods-use-this + _assertFunction(fn, res) { + var err; + try { + err = fn(res); + } catch (e) { + err = e; + } + if (err instanceof Error) return err; } -}; +} /** - * Perform assertions on the response status and return an Error upon failure. + * Wraps an assert function into another. + * The wrapper function edit the stack trace of any assertion error, prepending a more useful stack to it. * - * @param {Array} statusArray - * @param {Response} res - * @return {?Error} - * @api private + * @param {Function} assertFn + * @returns {Function} wrapped assert function */ -Test.prototype._assertStatusArray = function(statusArray, res) { - var b; - var expectedList; - if (!statusArray.includes(res.status)) { - b = http.STATUS_CODES[res.status]; - expectedList = statusArray.join(', '); - return new Error('expected one of "' + expectedList + '", got ' + res.status + ' "' + b + '"'); - } -}; +function wrapAssertFn(assertFn) { + var savedStack = new Error().stack.split('\n').slice(3); -/** - * Performs an assertion by calling a function and return an Error upon failure. - * - * @param {Function} fn - * @param {Response} res - * @return {?Error} - * @api private - */ -Test.prototype._assertFunction = function(fn, res) { - var err; - try { - err = fn(res); - } catch (e) { - err = e; - } - if (err instanceof Error) return err; -}; + return function(res) { + var badStack; + var err = assertFn(res); + if (err instanceof Error && err.stack) { + badStack = err.stack.replace(err.message, '').split('\n').slice(1); + err.stack = [err.toString()] + .concat(savedStack) + .concat('----') + .concat(badStack) + .join('\n'); + } + return err; + }; +} /** * Return an `Error` with `msg` and results properties. @@ -359,3 +343,9 @@ function error(msg, expected, actual) { err.showDiff = true; return err; } + +/** + * Expose `Test`. + */ + +module.exports = Test;