Files
elasticsearch-js/test/mocks/browser_http.js
2016-01-08 14:21:26 -07:00

482 lines
12 KiB
JavaScript

//////////////
/// Extended version of:
/// https://github.com/philikon/MockHttpRequest/
//////////////
/*
* Mock XMLHttpRequest (see http://www.w3.org/TR/XMLHttpRequest)
*
* Written by Philipp von Weitershausen <philipp@weitershausen.de>
* Released under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
*
* For test interaction it exposes the following attributes:
*
* - method, url, urlParts, async, user, password
* - requestText
*
* as well as the following methods:
*
* - getRequestHeader(header)
* - setResponseHeader(header, value)
* - receive(status, data)
* - err(exception)
* - authenticate(user, password)
*
*/
module.exports = MockHttpRequest;
var _ = require('lodash');
function MockHttpRequest() {
// These are internal flags and data structures
this.error = false;
this.sent = false;
this.requestHeaders = {};
this.responseHeaders = {};
}
MockHttpRequest.prototype = {
statusReasons: {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Moved Temporarily',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Time-out',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Request Entity Too Large',
414: 'Request-URI Too Large',
415: 'Unsupported Media Type',
416: 'Requested range not satisfiable',
417: 'Expectation Failed',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Time-out',
505: 'HTTP Version not supported',
507: 'Insufficient Storage'
},
/*** State ***/
UNSENT: 0,
OPENED: 1,
HEADERS_RECEIVED: 2,
LOADING: 3,
DONE: 4,
readyState: 0,
/*** Request ***/
open: function (method, url, async, user, password) {
if (typeof method !== 'string') {
throw 'INVALID_METHOD';
}
switch (method.toUpperCase()) {
case 'CONNECT':
case 'TRACE':
case 'TRACK':
throw 'SECURITY_ERR';
case 'DELETE':
case 'GET':
case 'HEAD':
case 'OPTIONS':
case 'POST':
case 'PUT':
method = method.toUpperCase();
}
this.method = method;
if (typeof url !== 'string') {
throw 'INVALID_URL';
}
this.url = url;
this.urlParts = this.parseUri(url);
if (async === undefined) {
async = true;
}
this.async = async;
this.user = user;
this.password = password;
this.readyState = this.OPENED;
this.onreadystatechange();
},
setRequestHeader: function (header, value) {
header = header.toLowerCase();
switch (header) {
case 'accept-charset':
case 'accept-encoding':
case 'connection':
case 'content-length':
case 'cookie':
case 'cookie2':
case 'content-transfer-encoding':
case 'date':
case 'expect':
case 'host':
case 'keep-alive':
case 'referer':
case 'te':
case 'trailer':
case 'transfer-encoding':
case 'upgrade':
case 'user-agent':
case 'via':
return;
}
if ((header.substr(0, 6) === 'proxy-') || (header.substr(0, 4) === 'sec-')) {
return;
}
// it's the first call on this header field
if (this.requestHeaders[header] === undefined) {
this.requestHeaders[header] = value;
}
else {
var prev = this.requestHeaders[header];
this.requestHeaders[header] = prev + ', ' + value;
}
},
send: function (data) {
if ((this.readyState !== this.OPENED) || this.sent) {
throw 'INVALID_STATE_ERR';
}
if ((this.method === 'GET') || (this.method === 'HEAD')) {
data = null;
}
//TODO set Content-Type header?
this.error = false;
this.sent = true;
this.onreadystatechange();
// fake send
this.requestText = data;
this.onsend();
},
abort: function () {
this.responseText = null;
this.error = true;
for (var header in this.requestHeaders) {
if (this.requestHeaders.hasOwnProperty(header)) {
delete this.requestHeaders[header];
}
}
delete this.requestText;
this.onreadystatechange();
this.onabort();
this.readyState = this.UNSENT;
},
/*** Response ***/
status: 0,
statusText: '',
getResponseHeader: function (header) {
if ((this.readyState === this.UNSENT) || (this.readyState === this.OPENED) || this.error) {
return null;
}
return this.responseHeaders[header.toLowerCase()];
},
getAllResponseHeaders: function () {
var r = '';
_.each(this.responseHeaders, function (header) {
if ((header === 'set-cookie') || (header === 'set-cookie2')) {
return;
}
//TODO title case header
r += header + ': ' + this.responseHeaders[header] + '\r\n';
}, this);
return r;
},
responseText: '',
responseXML: undefined, //TODO
/*** See http://www.w3.org/TR/progress-events/ ***/
onload: function () {
// Instances should override this.
},
onprogress: function () {
// Instances should override this.
},
onerror: function () {
// Instances should override this.
},
onabort: function () {
// Instances should override this.
},
onreadystatechange: function () {
// Instances should override this.
},
/*** Properties and methods for test interaction ***/
onsend: function () {
// Instances should override this.
},
getRequestHeader: function (header) {
return this.requestHeaders[header.toLowerCase()];
},
setResponseHeader: function (header, value) {
this.responseHeaders[header.toLowerCase()] = value;
},
makeXMLResponse: function (data) {
var xmlDoc;
// according to specs from point 3.7.5:
// '1. If the response entity body is null terminate these steps
// and return null.
// 2. If final MIME type is not null, text/xml, application/xml,
// and does not end in +xml terminate these steps and return null.
var mimetype = this.getResponseHeader('Content-Type');
mimetype = mimetype && mimetype.split(';', 1)[0];
if ((mimetype == null) || (mimetype === 'text/xml') ||
(mimetype === 'application/xml') ||
(mimetype && mimetype.substring(mimetype.length - 4) === '+xml')) {
// Attempt to produce an xml response
// and it will fail if not a good xml
try {
if (window.DOMParser) {
var parser = new DOMParser();
xmlDoc = parser.parseFromString(data, 'text/xml');
} else { // Internet Explorer
xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
xmlDoc.async = 'false';
xmlDoc.loadXML(data);
}
} catch (e) {
// according to specs from point 3.7.5:
// '3. Let document be a cookie-free Document object that
// represents the result of parsing the response entity body
// into a document tree following the rules from the XML
// specifications. If this fails (unsupported character
// encoding, namespace well-formedness error etc.), terminate
// these steps return null.'
xmlDoc = null;
}
// parse errors also yield a null.
if ((xmlDoc && xmlDoc.parseError && xmlDoc.parseError.errorCode !== 0) || (xmlDoc && xmlDoc.documentElement &&
xmlDoc.documentElement.nodeName !== 'parsererror') || (xmlDoc && xmlDoc.documentElement && xmlDoc.documentElement
.nodeName !== 'html' && xmlDoc.documentElement.firstChild && xmlDoc.documentElement.firstChild.nodeName ===
'body' && xmlDoc.documentElement.firstChild.firstChild && xmlDoc.documentElement.firstChild.firstChild.nodeName
=== 'parsererror')) {
xmlDoc = null;
}
} else {
// mimetype is specified, but not xml-ish
xmlDoc = null;
}
return xmlDoc;
},
// Call this to simulate a server response
receive: function (status, data) {
if ((this.readyState !== this.OPENED) || (!this.sent)) {
// Can't respond to unopened request.
throw 'INVALID_STATE_ERR';
}
this.status = status;
this.statusText = status + ' ' + this.statusReasons[status];
this.readyState = this.HEADERS_RECEIVED;
this.onprogress();
this.onreadystatechange();
this.responseText = data;
this.responseXML = this.makeXMLResponse(data);
this.readyState = this.LOADING;
this.onprogress();
this.onreadystatechange();
this.readyState = this.DONE;
this.onreadystatechange();
this.onprogress();
this.onload();
},
// Call this to simulate a request error (e.g. NETWORK_ERR)
err: function (exception) {
if ((this.readyState !== this.OPENED) || (!this.sent)) {
// Can't respond to unopened request.
throw 'INVALID_STATE_ERR';
}
this.responseText = null;
this.error = true;
_.each(this.requestHeaders, function (header) {
delete this.requestHeaders[header];
}, this);
this.readyState = this.DONE;
if (!this.async) {
throw exception;
}
this.onreadystatechange();
this.onerror();
},
// Convenience method to verify HTTP credentials
authenticate: function (user, password) {
if (this.user) {
return (user === this.user) && (password === this.password);
}
if (this.urlParts.user) {
return ((user === this.urlParts.user) && (password === this.urlParts.password));
}
// Basic auth. Requires existence of the 'atob' function.
var auth = this.getRequestHeader('Authorization');
if (auth === undefined) {
return false;
}
if (auth.substr(0, 6) !== 'Basic ') {
return false;
}
if (typeof atob !== 'function') {
return false;
}
auth = atob(auth.substr(6));
var pieces = auth.split(':');
var requser = pieces.shift();
var reqpass = pieces.join(':');
return (user === requser) && (password === reqpass);
},
// Parse RFC 3986 compliant URIs.
// Based on parseUri by Steven Levithan <stevenlevithan.com>
// See http://blog.stevenlevithan.com/archives/parseuri
parseUri: function (str) {
var pattern =
/^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/;
var key = ['source', 'protocol', 'authority', 'userInfo', 'user',
'password', 'host', 'port', 'relative', 'path',
'directory', 'file', 'query', 'anchor'
];
var querypattern = /(?:^|&)([^&=]*)=?([^&]*)/g;
var match = pattern.exec(str);
var uri = {};
var i = 14;
while (i--) {
uri[key[i]] = match[i] || '';
}
uri.queryKey = {};
uri[key[12]].replace(querypattern, function ($0, $1, $2) {
if ($1) {
uri.queryKey[$1] = $2;
}
});
return uri;
}
};
/*
* A small mock 'server' that intercepts XMLHttpRequest calls and
* diverts them to your handler.
*
* Usage:
*
* 1. Initialize with either
* var server = new MockHttpServer(your_request_handler);
* or
* var server = new MockHttpServer();
* server.handle = function (request) { ... };
*
* 2. Call server.start() to start intercepting all XMLHttpRequests.
*
* 3. Do your tests.
*
* 4. Call server.stop() to tear down.
*
* 5. Profit!
*/
function MockHttpServer(handler) {
if (handler) {
this.handle = handler;
}
}
MockHttpRequest.MockHttpServer = MockHttpServer;
MockHttpServer.prototype = {
start: function () {
var self = this;
function Request() {
var req = this;
req.onsend = function () {
req.sendStack = (new Error()).stack;
process.nextTick(function () {
self.handle(req);
});
};
MockHttpRequest.apply(this, arguments);
}
Request.prototype = MockHttpRequest.prototype;
window.OriginalHttpRequest = window.XMLHttpRequest;
window.XMLHttpRequest = Request;
},
stop: function () {
window.XMLHttpRequest = window.OriginalHttpRequest;
},
handle: function (request) {
// Instances should override this.
}
};