UNPKG

twreporter-react

Version:

React-Redux site for The Reporter Foundation in Taiwan

849 lines (717 loc) 24.3 kB
/* jshint strict:false */ /** * @module nock/scope */ var globalIntercept = require('./intercept') , mixin = require('./mixin') , matchBody = require('./match_body') , common = require('./common') , assert = require('assert') , url = require('url') , _ = require('lodash') , debug = require('debug')('nock.scope'); var fs; try { fs = require('fs'); } catch(err) { // do nothing, we're in the browser } function isStream(obj) { return (typeof obj !== 'undefined') && (typeof a !== 'string') && (! Buffer.isBuffer(obj)) && _.isFunction(obj.setEncoding); } function startScope(basePath, options) { var interceptors = {}, scope, transformPathFunction, transformRequestBodyFunction, matchHeaders = [], logger = debug, scopeOptions = options || {}, urlParts = url.parse(basePath), port = urlParts.port || ((urlParts.protocol === 'http:') ? 80 : 443), persist = false, contentLen = false, date = null, basePathname = urlParts.pathname.replace(/\/$/, ''); basePath = urlParts.protocol + '//' + urlParts.hostname + ':' + port; function add(key, interceptor, scope) { if (! interceptors.hasOwnProperty(key)) { interceptors[key] = []; } interceptors[key].push(interceptor); globalIntercept(basePath, interceptor, scope, scopeOptions, urlParts.hostname); } function remove(key, interceptor) { if (persist) { return; } var arr = interceptors[key]; if (arr) { arr.splice(arr.indexOf(interceptor), 1); if (arr.length === 0) { delete interceptors[key]; } } } function intercept(uri, method, requestBody, interceptorOptions) { var interceptorMatchHeaders = []; var key = method.toUpperCase() + ' ' + basePath + basePathname + uri; function replyWithError(errorMessage) { this.errorMessage = errorMessage; this.options = interceptorOptions || {}; for (var opt in scopeOptions) { if (_.isUndefined(this.options[opt])) { this.options[opt] = scopeOptions[opt]; } } add(key, this, scope, scopeOptions); return scope; } function reply(statusCode, body, headers) { if (arguments.length <= 2 && _.isFunction(statusCode)) { body = statusCode; statusCode = 200; } this.statusCode = statusCode; // We use lower-case headers throughout Nock. headers = common.headersFieldNamesToLowerCase(headers); this.options = interceptorOptions || {}; for (var opt in scopeOptions) { if (_.isUndefined(this.options[opt])) { this.options[opt] = scopeOptions[opt]; } } if (scope._defaultReplyHeaders) { headers = headers || {}; headers = mixin(scope._defaultReplyHeaders, headers); } if (date) { headers = headers || {}; headers['date'] = date.toUTCString(); } if (headers !== undefined) { this.headers = {}; this.rawHeaders = []; // makes sure all keys in headers are in lower case for (var key2 in headers) { if (headers.hasOwnProperty(key2)) { this.headers[key2.toLowerCase()] = headers[key2]; this.rawHeaders.push(key2); this.rawHeaders.push(headers[key2]); } } debug('reply.rawHeaders:', this.rawHeaders); } // If the content is not encoded we may need to transform the response body. // Otherwise we leave it as it is. if (!common.isContentEncoded(headers)) { if (body && typeof(body) !== 'string' && typeof(body) !== 'function' && !Buffer.isBuffer(body) && !isStream(body)) { try { body = JSON.stringify(body); if (!this.headers) { this.headers = {}; } if (!this.headers['content-type']) { this.headers['content-type'] = 'application/json'; } if (contentLen) { this.headers['content-length'] = body.length; } } catch(err) { throw new Error('Error encoding response body into JSON'); } } } this.body = body; add(key, this, scope, scopeOptions); return scope; } function replyWithFile(statusCode, filePath, headers) { if (! fs) { throw new Error('No fs'); } var readStream = fs.createReadStream(filePath); readStream.pause(); this.filePath = filePath; return reply.call(this, statusCode, readStream, headers); } var matchStringOrRegexp = function(target, pattern) { if (pattern instanceof RegExp) { return target.toString().match(pattern); } else { return target === pattern; } }; function match(options, body, hostNameOnly) { debug('match %j, body = %j', options, body); if (hostNameOnly) { return options.hostname === urlParts.hostname; } var method = options.method || 'GET' , path = options.path , matches , proto = options.proto; if (transformPathFunction) { path = transformPathFunction(path); } if (typeof(body) !== 'string') { body = body.toString(); } if (transformRequestBodyFunction) { body = transformRequestBodyFunction(body, this._requestBody); } var checkHeaders = function(header) { if (_.isFunction(header.value)) { return header.value(options.getHeader(header.name)); } return matchStringOrRegexp(options.getHeader(header.name), header.value); }; if (!matchHeaders.every(checkHeaders) || !interceptorMatchHeaders.every(checkHeaders)) { logger('headers don\'t match'); return false; } // Also match request headers // https://github.com/pgte/nock/issues/163 function reqheaderMatches(key) { // We don't try to match request headers if these weren't even specified in the request. if (! options.headers) { return true; } var reqHeader = this.reqheaders[key]; var header = options.headers[key]; if (header && (typeof header !== 'string') && header.toString) { header = header.toString(); } // We skip 'host' header comparison unless it's available in both mock and actual request. // This because 'host' may get inserted by Nock itself and then get recorder. // NOTE: We use lower-case header field names throughout Nock. if (key === 'host' && (_.isUndefined(header) || _.isUndefined(reqHeader))) { return true; } if (reqHeader && header) { if (_.isFunction(reqHeader)) { return reqHeader(header); } else if (matchStringOrRegexp(header, reqHeader)) { return true; } } debug('request header field doesn\'t match:', key, header, reqHeader); return false; } var reqHeadersMatch = ! this.reqheaders || Object.keys(this.reqheaders).every(reqheaderMatches.bind(this)); if (!reqHeadersMatch) { return false; } function reqheaderContains(header) { return _.has(options.headers, header); } var reqContainsBadHeaders = this.badheaders && _.some(this.badheaders, reqheaderContains); if (reqContainsBadHeaders) { return false; } var matchKey = method.toUpperCase() + ' '; // If we have a filtered scope then we use it instead reconstructing // the scope from the request options (proto, host and port) as these // two won't necessarily match and we have to remove the scope that was // matched (vs. that was defined). if (this.__nock_filteredScope) { matchKey += this.__nock_filteredScope; } else { matchKey += proto + '://' + options.host; if ( options.port && options.host.indexOf(':') < 0 && (options.port !== 80 || options.proto !== 'http') && (options.port !== 443 || options.proto !== 'https') ) { matchKey += ":" + options.port; } } matchKey += path; // Match query strings when using query() var matchQueries = true; var queryIndex = -1; var queries; if (this.queries && (queryIndex = matchKey.indexOf('?')) !== -1) { queries = matchKey.slice(queryIndex + 1).split('&'); // Only check for query string matches if this.queries is an object if (_.isObject(this.queries)) { // Make sure that you have an equal number of keys. We are // looping through the passed query params and not the expected values // if the user passes fewer query params than expected but all values // match this will throw a false positive. Testing that the length of the // passed query params is equal to the length of expected keys will prevent // us from doing any value checking BEFORE we know if they have all the proper // params if (Object.keys(this.queries).length !== queries.length) { matchQueries = false; } else { for (var i = 0; i < queries.length; i++) { var query = queries[i].split('='); if (query[1] === undefined || this.queries[ query[0] ] === undefined) { matchQueries = false; break; } var isMatch = matchStringOrRegexp(query[1], this.queries[ query[0] ]); matchQueries = matchQueries && !!isMatch; } } } // Remove the query string from the matchKey matchKey = matchKey.substr(0, queryIndex); } else if (this.queries) { // If we expected query strings but didn't get any then this isn't a match matchQueries = false; } matches = matchKey === this._key && matchQueries; // special logger for query() if (queryIndex !== -1) { logger('matching ' + matchKey + '?' + queries.join('&') + ' to ' + this._key + ' with query(' + JSON.stringify(this.queries) + '): ' + matches); } else { logger('matching ' + matchKey + ' to ' + this._key + ': ' + matches); } if (matches) { matches = (matchBody.call(options, this._requestBody, body)); if (!matches) { logger('bodies don\'t match: \n', this._requestBody, '\n', body); } } return matches; } function matchIndependentOfBody(options) { var method = options.method || 'GET' , path = options.path , proto = options.proto; if (transformPathFunction) { path = transformPathFunction(path); } var checkHeaders = function(header) { return options.getHeader && matchStringOrRegexp(options.getHeader(header.name), header.value); }; if (!matchHeaders.every(checkHeaders) || !interceptorMatchHeaders.every(checkHeaders)) { return false; } var matchKey = method + ' ' + proto + '://' + options.host + path; return this._key === matchKey; } function filteringPath() { if (_.isFunction(arguments[0])) { this.transformFunction = arguments[0]; } return this; } function discard() { if ((persist || this.counter > 0) && this.filePath) { this.body = fs.createReadStream(this.filePath); this.body.pause(); } if (!persist && this.counter < 1) { remove(this._key, this); } } function matchHeader(name, value) { interceptorMatchHeaders.push({ name: name, value: value }); return this; } function basicAuth(options) { var username = options['user']; var password = options['pass'] || ''; var name = 'authorization'; var value = 'Basic ' + new Buffer(username + ':' + password).toString('base64'); interceptorMatchHeaders.push({ name: name, value: value }); return this; } /** * Set query strings for the interceptor * @name query * @param Object Object of query string name,values (accepts regexp values) * @public * @example * // Will match 'http://zombo.com/?q=t' * nock('http://zombo.com').get('/').query({q: 't'}); */ function query(queries) { this.queries = this.queries || {}; // Allow all query strings to match this route if (queries === true) { this.queries = queries; } for (var q in queries) { if (_.isUndefined(this.queries[q])) { var value = queries[q]; switch (true) { case _.isNumber(value): // fall-though case _.isBoolean(value): value = value.toString(); break; case _.isUndefined(value): // fall-though case _.isNull(value): value = ''; break; case _.isString(value): value = encodeURIComponent(value); break; } q = encodeURIComponent(q); // everything else, incl. Strings and RegExp values are used 'as-is' this.queries[q] = value; } } return this; } /** * Set number of times will repeat the interceptor * @name times * @param Integer Number of times to repeat (should be > 0) * @public * @example * // Will repeat mock 5 times for same king of request * nock('http://zombo.com).get('/').times(5).reply(200, 'Ok'); */ function times(newCounter) { if (newCounter < 1) { return this; } this.counter = newCounter; return this; } /** * An sugar syntax for times(1) * @name once * @see {@link times} * @public * @example * nock('http://zombo.com).get('/').once.reply(200, 'Ok'); */ function once() { return this.times(1); } /** * An sugar syntax for times(2) * @name twice * @see {@link times} * @public * @example * nock('http://zombo.com).get('/').twice.reply(200, 'Ok'); */ function twice() { return this.times(2); } /** * An sugar syntax for times(3). * @name thrice * @see {@link times} * @public * @example * nock('http://zombo.com).get('/').thrice.reply(200, 'Ok'); */ function thrice() { return this.times(3); } /** * Delay the response by a certain number of ms. * * @param {integer} ms - Number of milliseconds to wait * @return {scope} - the current scope for chaining */ function delay(ms) { this.delayInMs = ms; return this; } /** * Delay the connection by a certain number of ms. * * @param {integer} ms - Number of milliseconds to wait * @return {scope} - the current scope for chaining */ function delayConnection(ms) { this.delayConnectionInMs = ms; return this; } /** * Make the socket idle for a certain number of ms (simulated). * * @param {integer} ms - Number of milliseconds to wait * @return {scope} - the current scope for chaining */ function socketDelay(ms) { this.socketDelayInMs = ms; return this; } var interceptor = { _key: key , counter: 1 , _requestBody: requestBody // We use lower-case header field names throughout Nock. , reqheaders: common.headersFieldNamesToLowerCase((options && options.reqheaders) || {}) , badheaders: common.headersFieldsArrayToLowerCase((options && options.badheaders) || []) , reply: reply , replyWithError: replyWithError , replyWithFile: replyWithFile , discard: discard , match: match , matchIndependentOfBody: matchIndependentOfBody , filteringPath: filteringPath , matchHeader: matchHeader , basicAuth: basicAuth , query: query , times: times , once: once , twice: twice , thrice: thrice , delay: delay , delayConnection: delayConnection , socketDelay: socketDelay }; return interceptor; } function get(uri, requestBody, options) { return intercept(uri, 'GET', requestBody, options); } function post(uri, requestBody, options) { return intercept(uri, 'POST', requestBody, options); } function put(uri, requestBody, options) { return intercept(uri, 'PUT', requestBody, options); } function head(uri, requestBody, options) { return intercept(uri, 'HEAD', requestBody, options); } function patch(uri, requestBody, options) { return intercept(uri, 'PATCH', requestBody, options); } function merge(uri, requestBody, options) { return intercept(uri, 'MERGE', requestBody, options); } function _delete(uri, requestBody, options) { return intercept(uri, 'DELETE', requestBody, options); } function pendingMocks() { return Object.keys(interceptors); } function isDone() { // if nock is turned off, it always says it's done if (! globalIntercept.isOn()) { return true; } var keys = Object.keys(interceptors); if (keys.length === 0) { return true; } else { var doneHostCount = 0; keys.forEach(function(key) { var doneInterceptorCount = 0; interceptors[key].forEach(function(interceptor) { var isRequireDoneDefined = !_.isUndefined(interceptor.options.requireDone); if (isRequireDoneDefined && interceptor.options.requireDone === false) { doneInterceptorCount += 1; } else if (persist && interceptor.interceptionCounter > 0) { doneInterceptorCount += 1; } }); if (doneInterceptorCount === interceptors[key].length ) { doneHostCount += 1; } }); return (doneHostCount === keys.length); } } function done() { assert.ok(isDone(), "Mocks not yet satisfied:\n" + pendingMocks().join("\n")); } function buildFilter() { var filteringArguments = arguments; if (arguments[0] instanceof RegExp) { return function(candidate) { if (candidate) { candidate = candidate.replace(filteringArguments[0], filteringArguments[1]); } return candidate; }; } else if (_.isFunction(arguments[0])) { return arguments[0]; } } function filteringPath() { transformPathFunction = buildFilter.apply(undefined, arguments); if (!transformPathFunction) { throw new Error('Invalid arguments: filtering path should be a function or a regular expression'); } return this; } function filteringRequestBody() { transformRequestBodyFunction = buildFilter.apply(undefined, arguments); if (!transformRequestBodyFunction) { throw new Error('Invalid arguments: filtering request body should be a function or a regular expression'); } return this; } function matchHeader(name, value) { // We use lower-case header field names throughout Nock. matchHeaders.push({ name: name.toLowerCase(), value: value }); return this; } function defaultReplyHeaders(headers) { this._defaultReplyHeaders = common.headersFieldNamesToLowerCase(headers); return this; } function log(newLogger) { logger = newLogger; return this; } function _persist() { persist = true; return this; } function shouldPersist() { return persist; } function replyContentLength() { contentLen = true; return this; } function replyDate(d) { date = d || new Date(); return this; } scope = { get: get , post: post , delete: _delete , put: put , merge: merge , patch: patch , head: head , intercept: intercept , done: done , isDone: isDone , filteringPath: filteringPath , filteringRequestBody: filteringRequestBody , matchHeader: matchHeader , defaultReplyHeaders: defaultReplyHeaders , log: log , persist: _persist , shouldPersist: shouldPersist , pendingMocks: pendingMocks , replyContentLength: replyContentLength , replyDate: replyDate }; return scope; } function cleanAll() { globalIntercept.removeAll(); return module.exports; } function loadDefs(path) { if (! fs) { throw new Error('No fs'); } var contents = fs.readFileSync(path); return JSON.parse(contents); } function load(path) { return define(loadDefs(path)); } function getStatusFromDefinition(nockDef) { // Backward compatibility for when `status` was encoded as string in `reply`. if (!_.isUndefined(nockDef.reply)) { // Try parsing `reply` property. var parsedReply = parseInt(nockDef.reply, 10); if (_.isNumber(parsedReply)) { return parsedReply; } } var DEFAULT_STATUS_OK = 200; return nockDef.status || DEFAULT_STATUS_OK; } function getScopeFromDefinition(nockDef) { // Backward compatibility for when `port` was part of definition. if (!_.isUndefined(nockDef.port)) { // Include `port` into scope if it doesn't exist. var options = url.parse(nockDef.scope); if (_.isNull(options.port)) { return nockDef.scope + ':' + nockDef.port; } else { if (parseInt(options.port) !== parseInt(nockDef.port)) { throw new Error('Mismatched port numbers in scope and port properties of nock definition.'); } } } return nockDef.scope; } function tryJsonParse(string) { try { return JSON.parse(string); } catch(err) { return string; } } function define(nockDefs) { var nocks = []; nockDefs.forEach(function(nockDef) { var nscope = getScopeFromDefinition(nockDef) , npath = nockDef.path , method = nockDef.method.toLowerCase() || "get" , status = getStatusFromDefinition(nockDef) , headers = nockDef.headers || {} , reqheaders = nockDef.reqheaders || {} , body = nockDef.body || '' , options = nockDef.options || {}; // We use request headers for both filtering (see below) and mocking. // Here we are setting up mocked request headers but we don't want to // be changing the user's options object so we clone it first. options = _.clone(options) || {}; options.reqheaders = reqheaders; // Response is not always JSON as it could be a string or binary data or // even an array of binary buffers (e.g. when content is enconded) var response; if (!nockDef.response) { response = ''; } else { response = _.isString(nockDef.response) ? tryJsonParse(nockDef.response) : nockDef.response; } var nock; if (body==="*") { nock = startScope(nscope, options).filteringRequestBody(function() { return "*"; })[method](npath, "*").reply(status, response, headers); } else { nock = startScope(nscope, options); // If request headers were specified filter by them. if (reqheaders !== {}) { for (var k in reqheaders) { nock.matchHeader(k, reqheaders[k]); } } if (nockDef.filteringRequestBody) { nock.filteringRequestBody(nockDef.filteringRequestBody); } nock.intercept(npath, method, body).reply(status, response, headers); } nocks.push(nock); }); return nocks; } module.exports = startScope; module.exports.cleanAll = cleanAll; module.exports.activate = globalIntercept.activate; module.exports.isActive = globalIntercept.isActive; module.exports.isDone = globalIntercept.isDone; module.exports.pendingMocks = globalIntercept.pendingMocks; module.exports.removeInterceptor = globalIntercept.removeInterceptor; module.exports.disableNetConnect = globalIntercept.disableNetConnect; module.exports.enableNetConnect = globalIntercept.enableNetConnect; module.exports.load = load; module.exports.loadDefs = loadDefs; module.exports.define = define;