UNPKG

frisby

Version:

Frisby.js v2.0: REST API Endpoint Testing built on Jasmine

415 lines (342 loc) 10 kB
'use strict'; // NPM const _ = require('lodash'); const fetch = require('node-fetch'); const FormData = require('form-data'); const TIMEOUT_DEFAULT = 5000; // Frisby const FrisbyResponse = require('./response'); const expectHandlers = require('./expects'); class FrisbySpec { constructor() { this._fetch; this._request; this._response; this._expects = []; this._timeout; this._setupDefaults = {}; } /** * Call function to do some setup for this spec/test */ use(fn) { fn(this); return this; } /** * Setup defaults (probably from globalSetup(), but can be also be called per test) */ setup(opts, replace) { this._setupDefaults = replace ? opts : _.merge(this._setupDefaults, opts); return this; } /** * Timeout getter/setter * * @param {number} timeout - Max. timeout in milliseconds */ timeout(timeout) { // GETTER if (!timeout) { return this._timeout || (this._setupDefaults.request && this._setupDefaults.request.timeout) || TIMEOUT_DEFAULT; } // SETTER this._timeout = timeout; return this; } /** * Load JSON directly for use * * @param {Object} json - JSON to use as HTTP response */ fromJSON(json) { let jsonString = JSON.stringify(json); // Prepare headers let headers = new fetch.Headers(); headers.set('Content-Type', 'application/json'); // Prepare Response object let fetchResponse = new fetch.Response(jsonString, { url: '/', status: 200, statusText: 'OK', headers: headers, size: jsonString.length, timeout: 0 }); this._response = new FrisbyResponse(fetchResponse); // Resolve as promise this._fetch = fetch.Promise.resolve(fetchResponse) .then(response => response.json()) .then(responseBody => { this._response._body = responseBody; this._runExpects(); return this._response; }); return this; } getBaseUrl() { return this._setupDefaults.request.baseUrl ? this._setupDefaults.request.baseUrl : false; } _formatUrl(url, urlEncode = true) { let newUrl = urlEncode ? encodeURI(url) : url; let baseUrl = this.getBaseUrl(); // Prepend baseUrl if set, and if URL supplied is a path if (url.startsWith('/') && baseUrl) { newUrl = baseUrl + url; } return newUrl; } _fetchParams(params = {}) { let fetchParams = _.merge({}, this._setupDefaults.request, params); // Form handling - send correct form headers if (params.body instanceof FormData) { fetchParams.headers = _.merge(fetchParams.headers, fetchParams.body.getHeaders()); } return fetchParams; } /** * Fetch given URL with params (passthru to 'fetch' API) */ fetch(url, params = {}, options = {}) { let fetchParams = this._fetchParams(params); this._request = new fetch.Request(this._formatUrl(url, options.urlEncode), fetchParams); this._fetch = fetch(this._request, { timeout: this.timeout() }) // 'timeout' is a node-fetch option .then(response => { this._response = new FrisbyResponse(response); // Auto-parse JSON if (response.headers.has('Content-Type') && response.headers.get('Content-Type').includes('json') && response.status !== 204) { return response.json(); } return response.text(); }).then(responseBody => { this._response._body = responseBody; this._runExpects(); return this._response; }); return this; } /** * GET convenience wrapper */ get(url, params) { return this.fetch(url, params); } /** * PATCH convenience wrapper */ patch(url, params) { return this._requestWithBody('PATCH', url, params); } /** * POST convenience wrapper */ post(url, params) { return this._requestWithBody('POST', url, params); } /** * PUT convenience wrapper */ put(url, params) { return this._requestWithBody('PUT', url, params); } /** * DELETE convenience wrapper */ del(url, params) { params = params || {}; params.method = 'delete'; return this.fetch(url, params); } /** * */ _requestWithBody(method, url, params) { let postParams = { method }; // Auto-encode JSON body if NOT FormData if (params && _.isObject(params.body)) { if (!(params.body instanceof FormData)) { params.body = JSON.stringify(params.body); } } // Auto-set 'body' from 'params' JSON if 'body' and 'headers' are not provided (assume sending raw body only) if (params && _.isUndefined(params.body) && _.isUndefined(params.headers)) { postParams.body = JSON.stringify(params); } return this.fetch(url, Object.assign(postParams, params || {})); } /** * Chain calls to execute after fetch() */ then(fn) { if (fn instanceof FrisbySpec) { return fn; } this._ensureHasFetched(); this._fetch = this._fetch.then(response => { let result = fn(response); if (result) { return result; } else { return response; } }); return this; } /** * Used for 'done' function in Jasmine async tests * Ensures any errors get pass */ done(doneFn) { this._ensureHasFetched(); this._fetch = this._fetch.then(() => doneFn()); return this; } /** * Custom error handler (Promise catch) */ catch(fn) { this._ensureHasFetched(); this._fetch = this._fetch.catch(err => fn(err)); return this; } /** * Return internal promise used by Frisby.js * Note: Using this will break the chainability of Frisby.js method calls */ promise() { return this._fetch; } /** * Run test expectations */ _runExpects() { // Run all expectations for(let i = 0; i < this._expects.length; i++) { this._expects[i].call(this, this._response); } return this; } /** * Inspectors (to inspect data that the test is returning) * ========================================================================== */ inspectResponse() { return this.then(() => { this.inspectLog("\nResponse:", this._response); }); } inspectRequest() { return this.then(() => { this.inspectLog("\nRequest:", this._request); }); } inspectRequestHeaders() { return this.then(() => { this.inspectLog("\n"); this.inspectLog('Request Headers:'); let headers = this._request.headers.raw(); for (let key in headers) { this.inspectLog("\t" + key + ': ' + headers[key]); } }); } inspectBody() { return this.then(() => { this.inspectLog("\nBody:", this._response.body); }); } inspectJSON() { return this.then(() => { this.inspectLog("\nJSON:", JSON.stringify(this._response.body, null, 4)); }); } inspectStatus() { return this.then(() => { this.inspectLog("\nStatus:", this._response.status); }); } inspectHeaders() { return this.then(() => { this.inspectLog("\n"); this.inspectLog('Response Headers:'); let headers = this._response.headers.raw(); for (let key in headers) { this.inspectLog("\t" + key + ': ' + headers[key]); } }); } inspectLog() { let params = Array.prototype.slice.call(arguments); console.log.apply(null, params); // eslint-disable-line no-console return this; } /** * Expectations (wrappers around Jasmine methods) * ========================================================================== */ /** * Add expectation for current test (expects) */ expect(expectName) { let expectArgs = Array.prototype.slice.call(arguments).slice(1); return this._getExpectRunner(expectName, expectArgs, true); } /** * Add negative expectation for current test (expects.not) */ expectNot(expectName) { let expectArgs = Array.prototype.slice.call(arguments).slice(1); return this._getExpectRunner(expectName, expectArgs, false); } /** * Private methods (not meant to be part of the public API, and NOT to be * relied upon by consuming code - these names may change!) * ========================================================================== */ /** * Used internally for expect and expectNot to add expectations and then run them */ _getExpectRunner(expectName, expectArgs, expectPass) { let expectHandler; if (_.isFunction(expectName)) { expectHandler = expectName; } else { expectHandler = expectHandlers[expectName]; if (typeof expectHandler === 'undefined') { throw new Error("Expectation '" + expectName + "' is not defined."); } } return this._addExpect(response => { let didFail = false; try { expectHandler.apply(this, [response].concat(expectArgs)); } catch(e) { didFail = true; // Re-throw error if pass is expected; else bury it if (expectPass === true) { throw e; } } if (!expectPass && !didFail) { let fnArgs = expectArgs.map(a => a.toString()).join(', '); throw new Error(`expectNot('${expectName}', ${fnArgs}) passed and was supposed to fail`); } }); } /** * Ensure fetch() has been called already */ _ensureHasFetched() { if (typeof this._fetch === 'undefined') { throw new Error('Frisby spec not started. You must call fetch() first to begin a Frisby test.'); } } /** * Add expectation to execute after HTTP call is done */ _addExpect(fn) { this._expects.push(fn); return this; } /** * Static methods (mainly ones that affect all Frisby tests) * ========================================================================== */ static addExpectHandler(expectName, expectFn) { expectHandlers[expectName] = expectFn; } static removeExpectHandler(expectName) { delete expectHandlers[expectName]; } } module.exports = FrisbySpec;