/**
* Test utilities.
*
* @module server/spec/utils
*/
const _ = require('lodash');
const enrichApiError = require('enrich-api-error');
const moment = require('moment');
const SuperRest = require('superrest');
const app = require('../app');
const chai = require('./chai');
const config = require('../../config');
const db = require('../db');
let databaseConnectionClosed = false;
const logger = config.logger('spec');
class EnrichedSuperRest extends SuperRest {
expect(res, ...args) {
try {
return super.expect(res, ...args);
} catch(err) {
throw enrichApiError(err, res);
}
}
}
const expect = exports.expect = chai.expect;
/**
* Ensures that a database record has been deleted by attempting to reload a fresh instance.
*
* @param {object} record - A database record (an instance of a Bookshelf model).
* @param {options} [options] - Options.
* @param {string} [options.idColumn="api_id"] - The column uniquely identifying the record.
* @returns {Promise} A promise that is resolved if no record with the same ID is found in the database, or rejected if one is found.
*/
exports.expectDeleted = async function(record, options) {
if (!record) {
throw new Error('Record is required');
}
options = options || {};
const idColumn = options.idColumn || 'api_id';
const id = record.get(idColumn);
if (!id) {
throw new Error('Record must have an ID');
}
const Model = record.constructor;
const freshRecord = await new Model({ [idColumn]: id }).fetch();
expect(freshRecord).to.equal(null);
};
/**
* Ensures that a response from the server is a correctly formatted error response
* and that it contains the expected error or list of errors.
*
* // Expect a single error.
* expectErrors(res, {
* code: 'something.wrong',
* message: 'It went wrong...'
* });
*
* // Expect a list of errors.
* expectErrors(res, [
* {
* code: 'something.wrong',
* message: 'It went wrong...'
* },
* {
* code: 'something.bad',
* message: 'What?'
* }
* ]);
*
* An error response is expected to:
*
* * Have the application/json content type
* * Be a JSON object with a single `errors` property that is an array of errors
*
* @param {Response} res - A response object from a test.
*
* @param {object|object[]} - A single error or a list of errors that is expected to
* be in the response. The response is expected to contain exactly this or these
* errors and no others. If a single error is given, the response's `errors` array
* is expected to contain exactly that error and no other. If a list is given, the
* order of the errors is not checked.
*/
exports.expectErrors = function(res, expectedErrorOrErrors) {
expect(res.get('Content-Type'), 'res.headers.Content-Type').to.match(/^application\/json/);
expect(res.body, 'res.body').to.be.an('object');
expect(res.body, 'res.body').to.have.all.keys('errors');
expect(res.body.errors, 'res.body.errors').to.be.an('array');
// Check that at least one expected error was provided.
const expectedErrors = exports.toArray(expectedErrorOrErrors);
expect(expectedErrors).to.have.lengthOf.at.least(1);
// Check that the errors in the response match with chai-objects
expect(res.body.errors).to.have.objects(expectedErrors);
};
/**
* Ensures that a database record has not changed by reloading a fresh instance and comparing its attributes.
*
* @param {object} record - A database record (an instance of a Bookshelf model).
* @param {object} [options] - Options.
* @param {string} [options.idColumn="api_id"] - The column uniquely identifying the record.
* @returns {Promise} A promise that is resolved if the record has not changed, or rejected if it has changed.
*/
exports.expectUnchanged = async function(record, options) {
if (!record) {
throw new Error('Record is required');
}
options = options || {};
const idColumn = options.idColumn || 'api_id';
const id = record.get(idColumn);
if (!id) {
throw new Error('Record must have an ID');
}
const Model = record.constructor;
const freshRecord = await new Model({ [idColumn]: id }).fetch();
expect(freshRecord).to.be.ok;
expect(freshRecord.attributes).to.eql(record.attributes);
};
exports.initSuperRest = function(options) {
return new EnrichedSuperRest(app, _.defaults({}, options, {
pathPrefix: '/api',
updateMethod: 'PATCH'
}));
};
exports.setUp = function() {
after(() => {
if (!databaseConnectionClosed) {
db.close();
databaseConnectionClosed = true;
}
});
};
exports.cleanDatabase = async function() {
const start = new Date().getTime();
// Sequences of tables to delete in order to avoid foreign key conflicts
const tablesToDelete = [
[ 'locations', 'users' ]
];
for (let tableList of tablesToDelete) {
await Promise.all(tableList.map(table => db.knex.raw(`DELETE FROM ${table};`)));
}
const duration = (new Date().getTime() - start) / 1000;
logger.debug(`Cleaned database in ${duration}s`);
}
exports.createRecord = async function(model, data) {
const resolved = await Promise.resolve(data);
const values = _.mapValues(resolved, value => {
if (moment.isMoment(value)) {
return value.toDate();
} else {
return value;
}
});
return new model(values).save();
};
exports.checkRecord = async function(model, id, options) {
if (!model) {
throw new Error('Model is required');
} else if (!id) {
throw new Error('Record ID is required');
}
const idColumn = _.get(options, 'idColumn', 'api_id');
const record = await new model().where(idColumn, id).fetch();
if (!record) {
throw new Error(`No database record found with ID ${id}`);
}
return record;
};
exports.toArray = function(value) {
return _.isArray(value) ? value : [ value ];
};