Migrate to express 4.x series

- Remove express logger
 - Error handler responds with application/[json|javascript]
 - Fix all tests relying on res.headers
 - assert.response based on request module
This commit is contained in:
Raul Ochoa 2016-09-26 18:09:27 +02:00
parent 8487008ee0
commit abc2f130c9
12 changed files with 166 additions and 66 deletions

3
app.js
View File

@ -81,7 +81,8 @@ if ( ! global.settings.base_url ) {
var version = require("./package").version;
var server = require('./app/server')();
server.listen(global.settings.node_port, global.settings.node_host, function() {
var listener = server.listen(global.settings.node_port, global.settings.node_host);
listener.on('listening', function() {
console.info('Using configuration file "%s"', configurationFile);
console.log(
"CartoDB SQL API %s listening on %s:%s PID=%d (%s)",

View File

@ -15,6 +15,7 @@
//
var express = require('express');
var bodyParser = require('body-parser');
var os = require('os');
var Profiler = require('step-profiler');
var StatsD = require('node-statsd').StatsD;
@ -50,7 +51,7 @@ require('./utils/date_to_json');
// jshint maxcomplexity:12
function App() {
var app = express.createServer();
var app = express();
var redisConfig = {
host: global.settings.redis_host,
@ -102,16 +103,6 @@ function App() {
}
};
app.use(global.log4js.connectLogger(global.log4js.getLogger(), _.defaults(loggerOpts, {level:'info'})));
} else {
// Express logger uses tokens as described here: http://www.senchalabs.org/connect/logger.html
express.logger.token('sql', function(req) {
return app.getSqlQueryFromRequestBody(req);
});
app.use(express.logger({
buffer: true,
format: global.settings.log_format ||
':remote-addr :method :req[Host]:url :status :response-time ms -> :res[Content-Type]'
}));
}
// Initialize statsD client if requested
@ -172,9 +163,12 @@ function App() {
});
}
app.use(express.bodyParser());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.enable('jsonp callback');
app.set("trust proxy", true);
app.disable('x-powered-by');
app.disable('etag');
// basic routing

View File

@ -25,14 +25,22 @@ module.exports = function handleException(err, res) {
// Force inline content disposition
res.header("Content-Disposition", 'inline');
if ( res.req && res.req.profiler ) {
res.req.profiler.done('finish');
res.header('X-SQLAPI-Profiler', res.req.profiler.toJSONString());
var req = res.req;
if (req && req.profiler ) {
req.profiler.done('finish');
res.header('X-SQLAPI-Profiler', req.profiler.toJSONString());
}
res.send(msg, getStatusError(pgErrorHandler, res.req));
res.header('Content-Type', 'application/json; charset=utf-8');
res.status(getStatusError(pgErrorHandler, req));
if (req.query && req.query.callback) {
res.jsonp(msg);
} else {
res.json(msg);
}
if ( res.req && res.req.profiler ) {
if (req && req.profiler) {
res.req.profiler.sendStats();
}
};

View File

@ -17,11 +17,12 @@
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"body-parser": "~1.14.2",
"cartodb-psql": "~0.6.0",
"cartodb-query-tables": "0.2.0",
"cartodb-redis": "0.13.1",
"debug": "2.2.0",
"express": "~2.5.11",
"express": "~4.13.3",
"log4js": "cartodb/log4js-node#cdb",
"lru-cache": "~2.5.0",
"node-statsd": "~0.0.7",

View File

@ -848,9 +848,9 @@ it('GET /api/v1/sql with SQL parameter and no format, ensuring content-dispositi
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.ok(/json/.test(ct), 'Default format is not JSON: ' + ct);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^inline/.test(cd), 'Default format is not disposed inline: ' + cd);
assert.equal(true, /filename=cartodb-query.json/gi.test(cd), 'Unexpected JSON filename: ' + cd);
done();
@ -865,9 +865,9 @@ it('POST /api/v1/sql with SQL parameter and no format, ensuring content-disposit
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.ok(/json/.test(ct), 'Default format is not JSON: ' + ct);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^inline/.test(cd), 'Default format is not disposed inline: ' + cd);
assert.equal(true, /filename=cartodb-query.json/gi.test(cd), 'Unexpected JSON filename: ' + cd);
done();
@ -881,9 +881,9 @@ it('GET /api/v1/sql with SQL parameter and no format, but a filename', function(
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.ok(/json/.test(ct), 'Default format is not JSON: ' + ct);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'Format with filename is not disposed as attachment: ' + cd);
assert.equal(true, /filename=x.json/gi.test(cd), 'Unexpected JSON filename: ' + cd);
done();
@ -959,7 +959,7 @@ it('GET /api/v1/sql ensure cross domain set on errors', function(done){
},{
status: 400
}, function(err, res){
var cd = res.header('Access-Control-Allow-Origin');
var cd = res.headers['access-control-allow-origin'];
assert.deepEqual(res.headers['content-type'], 'application/json; charset=utf-8');
assert.deepEqual(res.headers['content-disposition'], 'inline');
assert.equal(cd, '*');

View File

@ -18,10 +18,10 @@ it('CSV format', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'CSV is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.csv/gi.test(cd));
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.equal(true, /header=present/.test(ct), "CSV doesn't advertise header presence: " + ct);
var rows = res.body.split(/\r\n/);
@ -59,10 +59,10 @@ it('CSV format from POST', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'CSV is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.csv/gi.test(cd));
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.equal(true, /header=present/.test(ct), "CSV doesn't advertise header presence: " + ct);
done();
});
@ -75,10 +75,10 @@ it('CSV format, custom filename', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'CSV is not disposed as attachment: ' + cd);
assert.equal(true, /filename=mycsv.csv/gi.test(cd), cd);
var ct = res.header('Content-Type');
var ct = res.headers['content-type'];
assert.equal(true, /header=present/.test(ct), "CSV doesn't advertise header presence: " + ct);
var row0 = res.body.substring(0, res.body.search(/[\n\r]/)).split(',');
var checkFields = { name: true, cartodb_id: true, the_geom: true, the_geom_webmercator: true };

View File

@ -25,7 +25,7 @@ it('GET /api/v1/sql with SQL parameter, ensuring content-disposition set to geoj
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'GEOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.geojson/gi.test(cd));
done();
@ -40,7 +40,7 @@ it('POST /api/v1/sql with SQL parameter, ensuring content-disposition set to geo
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'GEOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.geojson/gi.test(cd));
done();
@ -54,7 +54,7 @@ it('uses the last format parameter when multiple are used', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /filename=cartodb-query.geojson/gi.test(cd));
done();
});
@ -67,7 +67,7 @@ it('uses custom filename', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /filename=x.geojson/gi.test(cd), cd);
done();
});
@ -157,7 +157,7 @@ it('null geometries in geojson output', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'GEOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.geojson/gi.test(cd));
var gjson = JSON.parse(res.body);

View File

@ -112,7 +112,7 @@ it('KML format, unauthenticated', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
var row0 = res.body;
@ -136,7 +136,7 @@ it('KML format, unauthenticated, POST', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
done();
@ -154,7 +154,7 @@ it('KML format, bigger than 81920 bytes', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
assert.ok(res.body.length > 81920, 'KML smaller than expected: ' + res.body.length);
@ -169,7 +169,7 @@ it('KML format, skipfields', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
var row0 = res.body;
@ -192,7 +192,7 @@ it('KML format, unauthenticated, custom filename', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
assert.equal(true, /filename=kmltest.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
var name = extractFolderName(res.body);
@ -208,7 +208,7 @@ it('KML format, authenticated', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /filename=cartodb-query.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
done();
});

View File

@ -20,7 +20,7 @@ it('SHP format, unauthenticated', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd));
var tmpfile = '/tmp/myshape.zip';
@ -47,7 +47,7 @@ it('SHP format, unauthenticated, POST', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd), 'Unexpected SHP filename: ' + cd);
done();
@ -65,7 +65,7 @@ it('SHP format, big size, POST', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd), 'Unexpected SHP filename: ' + cd);
assert.ok(res.body.length > 81920, 'SHP smaller than expected: ' + res.body.length);
@ -81,7 +81,7 @@ it('SHP format, unauthenticated, with custom filename', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=myshape.zip/gi.test(cd));
var tmpfile = '/tmp/myshape.zip';
@ -108,7 +108,7 @@ it('SHP format, unauthenticated, with custom, dangerous filename', function(done
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var fname = "b_______a";
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=b_______a.zip/gi.test(cd), 'Unexpected SHP filename: ' + cd);
var tmpfile = '/tmp/myshape.zip';
@ -134,7 +134,7 @@ it('SHP format, authenticated', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd));
var tmpfile = '/tmp/myshape.zip';
var writeErr = fs.writeFileSync(tmpfile, res.body, 'binary');
@ -260,7 +260,7 @@ it('SHP format, concurrently', function(done){
var concurrency = 1;
var waiting = concurrency;
function validate(err, res){
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SHP is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd));
var tmpfile = '/tmp/myshape.zip';
@ -309,7 +309,7 @@ it('point with null first', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /filename=cartodb-query.zip/gi.test(cd));
var tmpfile = '/tmp/myshape.zip';
var writeErr = fs.writeFileSync(tmpfile, res.body, 'binary');

View File

@ -17,9 +17,9 @@ it('GET /api/v1/sql with SVG format', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.ok(/filename=cartodb-query.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('<path d="M 0 768 L 1024 0" />') > 0, res.body );
// TODO: test viewBox
done();
@ -38,10 +38,10 @@ it('POST /api/v1/sql with SVG format', function(done){
method: 'POST'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SVG is not disposed as attachment: ' + cd);
assert.ok(/filename=cartodb-query.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('<path d="M 0 768 L 1024 0" />') > 0, res.body );
// TODO: test viewBox
done();
@ -60,9 +60,9 @@ it('GET /api/v1/sql with SVG format and custom filename', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.ok(/filename=mysvg.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('<path d="M 0 768 L 1024 0" />') > 0, res.body );
// TODO: test viewBox
done();
@ -80,9 +80,9 @@ it('GET /api/v1/sql with SVG format and centered point', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.ok(/filename=cartodb-query.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('cx="0" cy="0"') > 0, res.body );
// TODO: test viewBox
// TODO: test radius
@ -102,9 +102,9 @@ it('GET /api/v1/sql with SVG format and trimmed decimals', function(done){
method: 'GET'
},{ }, function(err, res){
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.ok(/filename=cartodb-query.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('<path d="M 0 768 L 1024 0 500.12 167.01" />') > 0, res.body );
// TODO: test viewBox
@ -115,10 +115,10 @@ it('GET /api/v1/sql with SVG format and trimmed decimals', function(done){
method: 'GET'
},{}, function(err, res) {
assert.equal(res.statusCode, 200, res.body);
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'SVG is not disposed as attachment: ' + cd);
assert.ok(/filename=cartodb-query.svg/gi.test(cd), cd);
assert.equal(res.header('Content-Type'), 'image/svg+xml; charset=utf-8');
assert.equal(res.headers['content-type'], 'image/svg+xml; charset=utf-8');
assert.ok( res.body.indexOf('<path d="M 0 768 L 1024 0 500.123 167.012" />') > 0, res.body );
// TODO: test viewBox
done();

View File

@ -35,7 +35,7 @@ it('GET two polygons sharing an edge as topojson', function(done){
status: 200
},
function(err, res) {
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'TOPOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.topojson/gi.test(cd));
var topojson = JSON.parse(res.body);
@ -140,7 +140,7 @@ it('null geometries', function(done){
status: 200
},
function(err, res) {
var cd = res.header('Content-Disposition');
var cd = res.headers['content-disposition'];
assert.equal(true, /^attachment/.test(cd), 'TOPOJSON is not disposed as attachment: ' + cd);
assert.equal(true, /filename=cartodb-query.topojson/gi.test(cd));
var topojson = JSON.parse(res.body);

View File

@ -1,6 +1,7 @@
var http = require('http');
var assert = module.exports = exports = require('assert');
var request = require('request');
/**
* Assert response from `server` with
@ -11,7 +12,7 @@ var assert = module.exports = exports = require('assert');
* @param {Object|Function} res
* @param {String|Function|Object} msg
*/
assert.response = function(server, req, res, msg){
assert.responseOld = function(server, req, res, msg){
var port = 5555;
function check(){
try {
@ -171,6 +172,101 @@ assert.response = function(server, req, res, msg){
}
};
assert.response = function(server, req, res, callback) {
if (!callback) {
callback = res;
res = {};
}
var port = 5555,
host = '127.0.0.1';
var listeningAttempts = 0;
var listener;
function listen() {
if (listeningAttempts > 25) {
return callback(new Error('Tried too many ports'));
}
listener = server.listen(port, host);
listener.on('error', function() {
port++;
listeningAttempts++;
listen();
});
listener.on('listening', onServerListening);
}
listen();
// jshint maxcomplexity:10
function onServerListening() {
var status = res.status || res.statusCode;
var requestParams = {
url: 'http://' + host + ':' + port + req.url,
method: req.method || 'GET',
headers: req.headers || {},
timeout: req.timeout || 0,
encoding: req.encoding || 'utf8'
};
if (req.body || req.data) {
requestParams.body = req.body || req.data;
}
request(requestParams, function assert$response$requestHandler(error, response, body) {
listener.close(function() {
if (error) {
return callback(error);
}
response = response || {};
response.body = response.body || body;
// Assert response body
if (res.body) {
var eql = res.body instanceof RegExp ? res.body.test(response.body) : res.body === response.body;
assert.ok(
eql,
colorize('[red]{Invalid response body.}\n' +
' Expected: [green]{' + res.body + '}\n' +
' Got: [red]{' + response.body + '}')
);
}
// Assert response status
if (typeof status === 'number') {
assert.equal(response.statusCode, status,
colorize('[red]{Invalid response status code.}\n' +
' Expected: [green]{' + status + '}\n' +
' Got: [red]{' + response.statusCode + '}\n' +
' Body: ' + response.body)
);
}
// Assert response headers
if (res.headers) {
var keys = Object.keys(res.headers);
for (var i = 0, len = keys.length; i < len; ++i) {
var name = keys[i],
actual = response.headers[name.toLowerCase()],
expected = res.headers[name],
headerEql = expected instanceof RegExp ? expected.test(actual) : expected === actual;
assert.ok(headerEql,
colorize('Invalid response header [bold]{' + name + '}.\n' +
' Expected: [green]{' + expected + '}\n' +
' Got: [red]{' + actual + '}')
);
}
}
// Callback
callback(null, response);
});
});
}
};
/**
* Colorize the given string using ansi-escape sequences.
* Disabled when --boring is set.