¶ Scenario Client ExtensionsScenario methods to make HTTP calls and manage request options. Related components:
|
|
¶ DependenciesThe following external libraries are used:
|
var _ = require('underscore'),
merge = require('deepmerge'),
q = require('q'),
slice = Array.prototype.slice;
|
¶ ExportsThis module exports an object with methods, to be used to extend the scenario prototype. |
module.exports = function(Client) {
return function(ScenarioPrototype) {
ScenarioPrototype.initializers.push(function() {
this.client = new Client();
this.requestFilters = [];
this.requestQueue = [];
|
¶ Client EventsAll events emitted by the scenario's HTTP client are also emitted
by the scenario object itself, with the
|
_.each([ 'request', 'error', 'response' ], function(event) {
this.client.on(event, _.bind(this.emit, this, 'client:' + event));
}, this);
this.on('configure', _.bind(this.client.emit, this.client, 'configure'));
this.on('configure', _.bind(this.onRequestConfigure, this));
});
ScenarioPrototype.beforeRun.push(function(runOptions) {
this.clearDefaultRequestOptions();
});
_.extend(ScenarioPrototype, {
|
¶ Methods |
|
¶ #request(options)Starts an HTTP request and returns a promise that will be resolved with the HTTP response or rejected if an error occurs. Return this promise in a scenario step and the HTTP response will be available to the next step.
See client.js for available options. This method has aliases for common HTTP methods. |
request: function(options) {
|
Default request options can be configured to be added to all requests. See #setDefaultRequestOptions. |
options = merge(this.defaultRequestOptions || {}, options || {});
|
A base URL can be configured to be automatically preprended to all URLs. See scenario options. |
if (this.baseUrl) {
options.url = options.url ? this.baseUrl + options.url : this.baseUrl;
}
|
Request filter functions can be configured to modify the request options prior to the request. See #addRequestFilter. |
if (this.requestFilters.length) {
options.filters = _.pluck(this.requestFilters, 'filter');
}
return this.queueRequest(options);
},
checkResponse: function(expected, response) {
this.checkStatusCode(expected.statusCode, response);
return response;
},
|
Check the status code by adding a
Check a range of codes with a regular expression:
You can also use an array of expected status codes.
To specify a custom error message, pass an object with the expected
value as the
To build an error message from the expected and actual values,
pass a function as the
|
checkStatusCode: function(expected, response) {
expected = _.isObject(expected) && _.has(expected, 'value') ? expected : { value: expected };
var expectedValues = expected.value;
if (!expectedValues || _.find(_.isArray(expectedValues) ? expectedValues : [ expectedValues ], function(statusCode) {
return _.isRegExp(statusCode) ? statusCode.test(response.statusCode) : statusCode === response.statusCode;
})) {
return;
}
throw new Error(this.statusCodeErrorMessage(expected.message, expectedValues, response.statusCode));
},
statusCodeErrorMessage: function(message, expected, actual) {
if (typeof(message) === 'function') {
return message(expected, actual);
} else if (message) {
return message;
} else {
return 'Expected server to respond with status code ' + this.statusCodeDescription(expected) + '; got ' + actual;
}
},
statusCodeDescription: function(expected) {
var description = expected;
if (_.isArray(expected)) {
description = _.reduce(expected, function(memo, s, i) {
return memo + (i === 0 ? '' : ',') + s.toString();
}, 'in [') + ']';
}
return description;
},
|
¶ #setDefaultRequestOptions(options)Sets the default request options to the specified options. All further requests will use these options by default.
|
setDefaultRequestOptions: function(options) {
this.defaultRequestOptions = options;
this.cleanRequestOptions(this.defaultRequestOptions);
},
|
¶ #extendDefaultRequestOptions(options)Extends the default request options with the specified additional options.
|
extendDefaultRequestOptions: function(options) {
this.defaultRequestOptions = _.extend({}, this.defaultRequestOptions, options);
this.cleanRequestOptions(this.defaultRequestOptions);
},
|
¶ #mergeDefaultRequestOptions(options)Merges additional options into the default request options. This is done with the deepmerge library.
|
mergeDefaultRequestOptions: function(options) {
this.defaultRequestOptions = merge(this.defaultRequestOptions, options);
this.cleanRequestOptions(this.defaultRequestOptions);
},
|
With both
|
cleanRequestOptions: function(options) {
_.each(options, function(value, key) {
if (value === undefined) {
delete options[key];
} else if (_.isObject(value)) {
this.cleanRequestOptions(value);
}
}, this);
},
|
¶ #clearDefaultRequestOptions(names...)Clears the default request options with the specified names.
|
clearDefaultRequestOptions: function() {
var names = slice.call(arguments);
|
Call this method with no arguments to clear all options. |
if (!names.length) {
delete this.defaultRequestOptions;
} else {
_.each(names, function(name) {
delete this.defaultRequestOptions[name];
}, this);
}
},
|
¶ #addRequestFilter(name, filter)Adds a filter function to process options before an HTTP request is started. The filter function will be passed the complete request options and is expected to return the processed options.
|
addRequestFilter: function(name, filter) {
var data = {};
|
Call this method with only a filter function to define an unnamed filter.
|
if (typeof(name) == 'function') {
data = { filter: name };
} else {
this.removeRequestFilters(name);
data = { name: name, filter: filter };
}
this.requestFilters.push(data);
},
|
¶ #removeRequestFilters(namesOrFunctions...)Removes the specified request filters. Arguments should be either filter names or filter functions.
|
removeRequestFilters: function() {
var toRemove = slice.call(arguments);
|
Call this method with no arguments to remove all filters. |
if (!toRemove.length) {
this.requestFilters = [];
return;
}
var namesToRemove = _.filter(toRemove, function(value) {
return _.isString(value);
}), functionsToRemove = _.filter(toRemove, function(value) {
return _.isFunction(value);
});
this.requestFilters = _.reject(this.requestFilters, function(filter) {
return _.contains(namesToRemove, filter.name) || _.contains(functionsToRemove, filter.filter);
});
},
queueRequest: function(options) {
var deferred = q.defer();
this.requestQueue.push(_.extend({}, options, { deferred: deferred }));
this.startNextRequest();
return deferred.promise;
},
startNextRequest: function() {
if (!this.requestQueue.length) {
return;
}
var pipeline = this.requestPipeline || 0,
cooldown = this.requestCooldown || 0,
delay = this.requestDelay || 0,
now = new Date().getTime();
if (delay && this.lastRequestStartTime && now - this.lastRequestStartTime < delay) {
if (!this.requestDelayTimeout) {
this.requestDelayTimeout = setTimeout(_.bind(this.startNextRequest, this), delay - (now - this.lastRequestStartTime));
}
return;
}
if (cooldown && this.requestCooldownTimeout) {
return;
}
if (pipeline) {
this.currentRequestCount = this.currentRequestCount || 0;
if (this.currentRequestCount >= pipeline) {
return;
}
}
this.lastRequestStartTime = new Date().getTime();
if (this.requestDelayTimeout) {
clearTimeout(this.requestDelayTimeout);
this.requestDelayTimeout = setTimeout(_.bind(this.startNextRequest, this), delay);
}
this.currentRequestCount++;
var options = this.requestQueue.shift();
var deferred = options.deferred;
delete options.deferred;
var promise = this.startRequest(options).fin(_.bind(function() {
this.currentRequestCount--;
this.lastRequestResponseTime = new Date().getTime();
if (this.requestCooldownTimeout) {
clearTimeout(this.requestCooldownTimeout);
}
this.requestCooldownTimeout = setTimeout(_.bind(function() {
delete this.requestCooldownTimeout;
this.startNextRequest();
}, this), cooldown);
this.startNextRequest();
}, this));
return promise.then(deferred.resolve, deferred.reject);
},
startRequest: function(options) {
|
Some response properties can be automatically checked using the |
var expect = options.expect || {};
delete options.expect;
return this.client.request(options).then(_.bind(this.checkResponse, this, expect));
},
onRequestConfigure: function(options) {
this.validateRequestOptions(options);
if (_.has(options, 'baseUrl')) {
this.baseUrl = options.baseUrl;
}
if (options.defaultRequestOptions) {
this.setDefaultRequestOptions(options.defaultRequestOptions);
}
_.extend(this, _.pick(options, 'requestPipeline', 'requestCooldown', 'requestDelay'));
},
validateRequestOptions: function(runOptions) {
_.each([ 'requestPipeline', 'requestCooldown', 'requestDelay' ], _.bind(this.validateNumberOption, this, runOptions));
if (_.has(runOptions, 'requestPipeline') && runOptions.requestPipeline <= 0) {
throw new Error('The `requestPipeline` option must be greater than 0; got ' + runOptions.requestPipeline);
} else if (runOptions.requestCooldown && runOptions.requestCooldown < 0) {
throw new Error('The `requestCooldown` option must be greater than or equal to 0; got ' + runOptions.requestCooldown);
} else if (runOptions.requestDelay && runOptions.requestDelay < 0) {
throw new Error('The `requestDelay` option must be greater than or equal to 0; got ' + runOptions.requestDelay);
}
},
validateNumberOption: function(options, name) {
if (_.has(options, name) && (isNaN(options[name]) || typeof(options[name]) != 'number')) {
throw new Error('Expected `' + name + '` option to be a number, got ' + options[name] + ' (' + typeof(options[name]) + ')');
}
}
});
|
¶ Request AliasesThe |
_.each([ 'get', 'head', 'post', 'put', 'patch', 'delete' ], function(method) {
ScenarioPrototype[method] = function(options) {
return this.request(_.extend({}, options, { method: method.toUpperCase() }));
};
});
};
};
module.exports['@require'] = [ 'client' ];
|