const _ = require('lodash');
const supertest = require('supertest');
/**
* [SuperTest](https://github.com/visionmedia/supertest#readme) helpers to test REST APIs.
*
* @class
* @see https://github.com/visionmedia/supertest#readme
*/
class SuperRest {
/**
* Returns a SuperREST instance to test the specified application.
*
* @constructs
*
* @param {Application} app - The application to test.
*
* @param {object} [options] - SuperREST configuration that applies to your entire API.
*
* @param {string|RegExp} [options.expectedContentType] - The default Content-Type header that the server is expected to use in responses.
* An exact match is required if it's a string.
*
* @param {string} [options.pathPrefix] - A prefix common to all your API routes.
* If given at construction, you won't have to repeat it for each test.
*
* @param {string} [options.updateMethod="PUT"] - The HTTP method used when calling the `update` method.
* You might want to use `PATCH` if your API uses only that, or use the `patch` method instead.
*/
constructor(app, options) {
options = options || {};
this.app = app;
this.expectedContentType = options.expectedContentType;
this.pathPrefix = options.pathPrefix || '';
this.updateMethod = options.updateMethod || 'PUT';
}
/**
* Starts and returns a SuperTest chain.
*
* @method
*
* @param {string} [method="GET"] - The HTTP method.
*
* @param {string} path - The path of the API resource to test.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} [body] - The request body to send to the server, if any.
*
* @param {object} [options] - Test options.
*
* @param {string|RegExp} [options.expectedContentType] - The Content-Type header expected
* to be found in the response. An exact match is required if it's a string. Overrides the
* `expectedContentType` option given to the constructor.
*
* @param {number} [options.expectedStatusCode=200] - The expected HTTP status code of the
* response.
*
* @param {boolean|string} [options.pathPrefix] - A path prefix to use for this specific test.
* Overrides the `pathPrefix` option given to the constructor. If false and a `pathPrefix`
* option was given to the constructor, it is not used (the `path` argument is used as is).
*/
test(method, path, body, options) {
options = options || {};
let test = supertest(this.app);
const testMethod = (method || 'GET').toLowerCase();
if (typeof(test[testMethod]) != 'function') {
throw new Error(`supertest has no "${testMethod}" function`);
}
let testPath = path;
if (options.pathPrefix) {
testPath = `${options.pathPrefix}${testPath}`;
} else if (this.pathPrefix && (options.pathPrefix === undefined || options.pathPrefix === true)) {
testPath = `${this.pathPrefix}${testPath}`;
}
test = test[testMethod](testPath);
if (body) {
test = test.send(body);
}
test = test.expect(res => {
this.expect(res, options);
});
return test;
}
/**
* Make default RESTful assertions on a SuperTest response.
*
* This method is used by {@link SuperRest#test} (and all CRUD aliases).
* You may override it to perform additional assertions.
*
* @method
*
* @param {Response} res - A SuperTest response.
*
* @param {object} [options] - Assertion options.
*
* @param {string|RegExp} [options.expectedContentType] - The Content-Type header expected
* to be found in the response. An exact match is required if it's a string. Overrides the
* `expectedContentType` option given to the constructor.
*
* @param {number} [options.expectedStatusCode=200] - The expected HTTP status code of the
* response.
*/
expect(res, options) {
const expectedStatus = options.expectedStatus !== undefined ? options.expectedStatus : 200;
if (res.status !== expectedStatus) {
throw new Error(`Expected HTTP status code ${res.status} to equal ${expectedStatus}`);
}
const expectedContentType = options.expectedContentType !== undefined ? options.expectedContentType : this.expectedContentType;
if (_.isString(expectedContentType) && res.get('Content-Type') !== expectedContentType) {
throw new Error(`Expected HTTP Content-Type header "${res.get('Content-Type')}" to equal "${expectedContentType}"`);
} else if (_.isRegExp(expectedContentType) && !res.get('Content-Type')) {
throw new Error(`Expected missing HTTP Content-Type header to match ${expectedContentType}`);
} else if (_.isRegExp(expectedContentType) && !res.get('Content-Type').match(expectedContentType)) {
throw new Error(`Expected HTTP Content-Type header "${res.get('Content-Type')}" to match ${expectedContentType}`);
}
}
/**
* Makes a POST request to create a resource with the specified body. The response
* is expected to have the status code HTTP 201 Created by default.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} body - The request body to send to the server.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*
* @param {number} [options.expectedStatusCode=201] - The expected HTTP status code of the
* response.
*/
create(path, body, options) {
return this.test('POST', path, body, _.defaults({}, options, {
expectedStatus: 201
}));
}
/**
* Makes a GET request to read a resource.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
read(path, options) {
return this.test('GET', path, undefined, options);
}
/**
* Makes a GET request to retrieve a resource.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
retrieve(...args) {
return this.read(...args);
}
/**
* Makes a PUT request to update a resource with the specified body.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} body - The request body to send to the server.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
update(path, body, options) {
options = options || {};
return this.test(options.method || this.updateMethod, path, body, options);
}
/**
* Makes a PATCH request to partially update a resource with the specified body.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} body - The request body to send to the server.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
patch(path, body, options) {
return this.test('PATCH', path, body, options);
}
/**
* Makes a DELETE request to delete a resource.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} [body] - An optional request body to send to the server.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
delete(path, body, options) {
return this.test('DELETE', path, body, options);
}
/**
* Makes a DELETE request to destroy a resource.
*
* @method
*
* @param {string} path - The path of the API resource.
* If a non-false `pathPrefix` option is given to the constructor or to this method,
* it will be prepended to the path to form the full test path.
*
* @param {*} [body] - An optional request body to send to the server.
*
* @param {object} [options] - Assertion options (see {@link SuperRest#test} for all options).
*/
destroy(...args) {
return this.delete(...args);
}
}
module.exports = SuperRest;