restler
Version:
An HTTP client library for node.js
757 lines (668 loc) • 24.6 kB
JavaScript
var rest = require('../lib/restler'),
http = require('http'),
util = require('util'),
path = require('path'),
fs = require('fs'),
crypto = require('crypto');
var port = 9000;
var hostname = 'localhost';
var host = 'http://' + hostname + ':' + port;
var nodeunit = require('nodeunit');
nodeunit.assert.re = function(actual, expected, message) {
if (!new RegExp(expected).test(actual)) {
nodeunit.assert.fail(actual, expected, message, '~=', nodeunit.assert.re);
}
};
function setup(response) {
return function(next) {
this.server = http.createServer(response);
this.server.listen(port, hostname, next);
};
}
function teardown() {
return function (next) {
if (this.server._handle) {
this.server.close();
}
process.nextTick(next);
};
}
function echoResponse(request, response) {
if (request.headers['x-connection-abort'] == 'true') {
request.connection.destroy();
return;
}
var echo = [];
echo.push(request.method + ' ' + request.url + ' HTTP/' + request.httpVersion);
for (var header in request.headers) {
echo.push(header + ': ' + request.headers[header]);
}
echo.push('', '');
echo = echo.join('\r\n');
request.addListener('data', function(chunk) {
echo += chunk.toString('binary');
});
request.addListener('end', function() {
response.writeHead(request.headers['x-status-code'] || 200, {
'content-type': 'text/plain',
'content-length': echo.length,
'request-method': request.method.toLowerCase()
});
setTimeout(function() {
response.end(request.method == 'HEAD' ? undefined : echo);
}, request.headers['x-delay'] | 0);
});
}
module.exports['Basic'] = {
setUp: setup(echoResponse),
tearDown: teardown(),
'Should GET': function (test) {
rest.get(host).on('complete', function(data) {
test.re(data, /^GET/, 'should be GET');
test.done();
});
},
'Should PATCH': function(test) {
rest.patch(host).on('complete', function(data) {
test.re(data, /^PATCH/, 'should be PATCH');
test.done();
});
},
'Should PUT': function(test) {
rest.put(host).on('complete', function(data) {
test.re(data, /^PUT/, 'should be PUT');
test.done();
});
},
'Should POST': function(test) {
rest.post(host).on('complete', function(data) {
test.re(data, /^POST/, 'should be POST');
test.done();
});
},
'Should DELETE': function(test) {
rest.del(host).on('complete', function(data) {
test.re(data, /^DELETE/, 'should be DELETE');
test.done();
});
},
'Should HEAD': function(test) {
rest.head(host).on('complete', function(data, response) {
test.equal(response.headers['request-method'], 'head', 'should be HEAD');
test.done();
});
},
'Should GET withouth path': function(test) {
rest.get(host).on('complete', function(data) {
test.re(data, /^GET \//, 'should hit /');
test.done();
});
},
'Should GET path': function(test) {
rest.get(host + '/thing').on('complete', function(data) {
test.re(data, /^GET \/thing/, 'should hit /thing');
test.done();
});
},
'Should preserve query string in url': function(test) {
rest.get(host + '/thing?boo=yah').on('complete', function(data) {
test.re(data, /^GET \/thing\?boo\=yah/, 'should hit /thing?boo=yah');
test.done();
});
},
'Should serialize query': function(test) {
rest.get(host, { query: { q: 'balls' } }).on('complete', function(data) {
test.re(data, /^GET \/\?q\=balls/, 'should hit /?q=balls');
test.done();
});
},
'Should POST body': function(test) {
rest.post(host, { data: 'balls' }).on('complete', function(data) {
test.re(data, /\r\n\r\nballs/, 'should have balls in the body');
test.done();
});
},
'Should POST buffer body': function(test) {
rest.post(host, { data: new Buffer('balls') }).on('complete', function(data) {
test.re(data, /\r\n\r\nballs/, 'should have balls in the body');
test.done();
});
},
'Should serialize POST body': function(test) {
rest.post(host, { data: { q: 'balls' } }).on('complete', function(data) {
test.re(data, /content-type\: application\/x-www-form-urlencoded/, 'should set content-type');
test.re(data, /content-length\: 7/, 'should set content-length');
test.re(data, /\r\n\r\nq=balls/, 'should have balls in the body');
test.done();
});
},
'Should send headers': function(test) {
rest.get(host, {
headers: { 'Content-Type': 'application/json' }
}).on('complete', function(data) {
test.re(data, /content\-type\: application\/json/, 'should have "content-type" header');
test.done();
});
},
'Should send Bearer auth': function(test) {
rest.post(host, { accessToken: 't0k3n' }).on('complete', function(data) {
test.re(data, /authorization\: Bearer t0k3n/, 'should have "authorization "header');
test.done();
});
},
'Should send basic auth': function(test) {
rest.post(host, { username: 'danwrong', password: 'flange' }).on('complete', function(data) {
test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization "header');
test.done();
});
},
'Should send basic auth with blank password': function(test) {
rest.post(host, { username: 'danwrong', password: '' }).on('complete', function(data) {
test.re(data, /authorization\: Basic ZGFud3Jvbmc6/, 'should have "authorization "header');
test.done();
});
},
'Should send basic auth if in url': function(test) {
rest.post('http://danwrong:flange@' + hostname + ':' + port).on('complete', function(data) {
test.re(data, /authorization\: Basic ZGFud3Jvbmc6Zmxhbmdl/, 'should have "authorization" header');
test.done();
});
},
'Should fire 2XX and 200 events': function(test) {
test.expect(3);
rest.get(host).on('2XX', function() {
test.ok(true);
}).on('200', function() {
test.ok(true);
}).on('complete', function() {
test.ok(true);
test.done();
});
},
'Should fire fail, 4XX and 404 events for 404': function(test) {
test.expect(4);
rest.get(host, { headers: { 'x-status-code': 404 }}).on('fail', function() {
test.ok(true);
}).on('4XX', function() {
test.ok(true);
}).on('404', function() {
test.ok(true);
}).on('complete', function() {
test.ok(true);
test.done();
});
},
'Should fire error and complete events on connection abort': function(test) {
test.expect(2);
rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('error', function() {
test.ok(true);
}).on('complete', function() {
test.ok(true);
test.done();
});
},
'Should correctly retry': function(test) {
var counter = 0;
rest.get(host, { headers: { 'x-connection-abort': 'true' }}).on('complete', function() {
if (++counter < 3) {
this.retry(10);
} else {
test.ok(true);
test.done();
}
});
},
'Should correctly retry after abort': function(test) {
var counter = 0;
rest.get(host).on('complete', function() {
if (++counter < 3) {
this.retry().abort();
} else {
test.ok(true);
test.done();
}
}).abort();
},
'Should correctly retry while pending': function(test) {
var counter = 0, request;
function command() {
var args = [].slice.call(arguments);
var method = args.shift();
if (method) {
setTimeout(function() {
request[method]();
command.apply(null, args);
}, 50);
}
}
request = rest.get(host, { headers: { 'x-delay': '1000' } }).on('complete', function() {
if (++counter < 3) {
command('retry', 'abort');
} else {
test.ok(true);
test.done();
}
});
command('abort');
}
};
module.exports['Multipart'] = {
setUp: setup(echoResponse),
tearDown: teardown(),
'Test multipart request with simple vars': function(test) {
rest.post(host, {
data: { a: 10, b: 'thing' },
multipart: true
}).on('complete', function(data) {
test.re(data, /content-type\: multipart\/form-data/, 'should set "content-type" header');
test.re(data, /name="a"(\s)+10/, 'should send a=10');
test.re(data, /name="b"(\s)+thing/, 'should send b=thing');
test.re(data, /content-length: 200/, 'should send content-length header');
test.done();
});
},
'Test multipart request with Data vars': function(test) {
rest.post(host, {
data: {
a: 10,
b: rest.data('b.txt', 'text/plain', 'thing'),
c: rest.data('c.txt', 'text/plain', new Buffer('thing'))
},
multipart: true
}).on('complete', function(data) {
test.re(data, /content-type\: multipart\/form-data/, 'should set "content-type" header');
test.re(data, /name="a"(\s)+10/, 'should send a=10');
test.re(data, /name="b"; filename="b.txt"\s+Content-Length: 5\s+Content-Type: text\/plain\s+thing\s/, 'should send b=thing');
test.re(data, /name="c"; filename="c.txt"\s+Content-Length: 5\s+Content-Type: text\/plain\s+thing\s/, 'should send c=thing');
test.re(data, /content-length: 410/, 'should send content-length header');
test.done();
});
},
};
function dataResponse(request, response) {
switch (request.url) {
case '/json':
response.writeHead(200, { 'content-type': 'application/json' });
response.end('{ "ok": true }');
break;
case '/xml':
response.writeHead(200, { 'content-type': 'application/xml' });
response.end('<document><ok>true</ok></document>');
break;
case '/big-xml':
response.writeHead(200, { 'content-type': 'application/xml' });
response.end('<documents type="array"><document><ok>true</ok></document></documents>');
break;
case '/yaml':
response.writeHead(200, { 'content-type': 'application/yaml' });
response.end('ok: true');
break;
case '/gzip':
response.writeHead(200, { 'content-encoding': 'gzip' });
response.end(Buffer('H4sIAAAAAAAAA0vOzy0oSi0uTk1RSEksSUweHFwADdgOgJYAAAA=', 'base64'));
break;
case '/deflate':
response.writeHead(200, { 'content-encoding': 'deflate' });
response.end(Buffer('eJxLzs8tKEotLk5NUUhJLElMHhxcAI9GO1c=', 'base64'));
break;
case '/truth':
response.writeHead(200, {
'content-encoding': 'deflate',
'content-type': 'application/json'
});
response.end(Buffer('eJw1i0sKgDAQQ++S9Sj+cDFXEReCoy2UCv0oIt7dEZEsEvKSC4eZErjoCTaCr5uQjICHilQjYfLxkAD+g/IN3BCcXXT3GSF7u0uI2vjs3HubwW1ZdwRRcCZj/QpOIcv9ACXbJLo=', 'base64'));
break;
case '/binary':
response.writeHead(200);
response.end(Buffer([9, 30, 64, 135, 200]));
break;
case '/push-json':
var echo = '';
request.addListener('data', function(chunk) {
echo += chunk.toString('binary');
});
request.addListener('end', function() {
response.writeHead(200, {
'content-type': 'application/json'
});
response.end(JSON.stringify(JSON.parse(echo)));
});
break;
case '/custom-mime':
response.writeHead(200, {
'content-type': 'application/vnd.github.beta.raw+json; charset=UTF-8'
});
response.end(JSON.stringify([6,6,6]));
break;
case '/mal-json':
response.writeHead(200, { 'content-type': 'application/json' });
response.end('Чебурашка');
break;
case '/mal-xml':
response.writeHead(200, { 'content-type': 'application/xml' });
response.end('Чебурашка');
break;
case '/mal-yaml':
response.writeHead(200, { 'content-type': 'application/yaml' });
response.end('{Чебурашка');
break;
case '/abort':
setTimeout(function() {
response.writeHead(200);
response.end('not aborted');
}, 100);
break;
case '/charset':
response.writeHead(200, {
'content-type': 'text/plain; charset=windows-1251'
});
response.end(Buffer('e0e1e2e3e4e5b8e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff', 'hex'));
break;
case '/timeout':
setTimeout(function() {
response.writeHead(200);
response.end('that took a while');
}, 100);
break;
default:
response.writeHead(404);
response.end();
}
}
module.exports['Deserialization'] = {
setUp: setup(dataResponse),
tearDown: teardown(),
'Should parse JSON': function(test) {
rest.get(host + '/json').on('complete', function(data) {
test.equal(data.ok, true, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should parse XML': function(test) {
rest.get(host + '/xml').on('complete', function(data, response) {
test.equal(data.document.ok[0], 'true', 'returned: ' + response.raw + ' parsed to ' + util.inspect(data));
test.done();
});
},
'Should parse XML with xml2js options': function(test) {
rest.get(host + '/big-xml', {xml2js: {explicitArray: false,ignoreAttrs: true}}).on('complete', function(data, response) {
test.equal(data.documents.document.ok, 'true', 'returned: ' + response.raw + ' parsed to ' + util.inspect(data));
test.done();
});
},
'Should parse YAML': function(test) {
rest.get(host + '/yaml').on('complete', function(data) {
test.equal(data.ok, true, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should gunzip': function(test) {
rest.get(host + '/gzip').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should inflate': function(test) {
rest.get(host + '/deflate').on('complete', function(data) {
test.re(data, /^(compressed data){10}$/, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should decode and parse': function(test) {
rest.get(host + '/truth').on('complete', function(data) {
var expected = {
what: -6,
is: {},
the: [ 0, 0, 0 ],
answer: 'answer',
to: 2,
life: 'life',
universe: null,
and: 3.14,
everything: true
};
test.deepEqual(data, expected, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should decode as buffer': function(test) {
rest.get(host + '/binary', { decoding: 'buffer' }).on('complete', function(data) {
test.ok(data instanceof Buffer, 'should be buffer');
test.equal(data.toString('base64'), 'CR5Ah8g=', 'returned: ' + util.inspect(data));
test.done();
});
},
'Should decode as binary': function(test) {
rest.get(host + '/binary', { decoding: 'binary' }).on('complete', function(data) {
test.ok(typeof data == 'string', 'should be string: ' + util.inspect(data));
test.equal(data, '\t\u001e@È', 'returned: ' + util.inspect(data));
test.done();
});
},
'Should decode as base64': function(test) {
rest.get(host + '/binary', { decoding: 'base64' }).on('complete', function(data) {
test.ok(typeof data == 'string', 'should be string: ' + util.inspect(data));
test.equal(data, 'CR5Ah8g=', 'returned: ' + util.inspect(data));
test.done();
});
},
'Should post and parse JSON': function(test) {
var obj = { secret : 'very secret string' };
rest.post(host + '/push-json', {
headers: {
'content-type': 'application/json'
},
data: JSON.stringify(obj)
}).on('complete', function(data) {
test.equal(obj.secret, data.secret, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should post and parse JSON via shortcut method': function(test) {
var obj = { secret : 'very secret string' };
rest.postJson(host + '/push-json', obj).on('complete', function(data) {
test.equal(obj.secret, data.secret, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should put and parse JSON via shortcut method': function(test) {
var obj = { secret : 'very secret string' };
rest.putJson(host + '/push-json', obj).on('complete', function(data) {
test.equal(obj.secret, data.secret, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should patch and parse JSON via shortcut method': function(test) {
var obj = { secret : 'very secret string' };
rest.patchJson(host + '/push-json', obj).on('complete', function(data) {
test.equal(obj.secret, data.secret, 'returned: ' + util.inspect(data));
test.done();
});
},
'Should understand custom mime-type': function(test) {
rest.parsers.auto.matchers['application/vnd.github+json'] = function(data, callback) {
rest.parsers.json.call(this, data, function(err, data) {
if (!err) {
data.__parsedBy__ = 'github';
}
callback(err, data);
});
};
rest.get(host + '/custom-mime').on('complete', function(data) {
test.expect(3);
test.ok(Array.isArray(data), 'should be array, returned: ' + util.inspect(data));
test.equal(data.join(''), '666', 'should be [6,6,6], returned: ' + util.inspect(data));
test.equal(data.__parsedBy__, 'github', 'should use vendor-specific parser, returned: ' + util.inspect(data.__parsedBy__));
test.done();
});
},
'Should correctly soft-abort request': function(test) {
test.expect(4);
rest.get(host + '/abort').on('complete', function(data) {
test.equal(data, null, 'data should be null');
test.equal(this.aborted, true, 'should be aborted');
test.done();
}).on('error', function(err) {
test.ok(false, 'should not emit error event');
}).on('abort', function(err) {
test.equal(err, null, 'err should be null');
test.equal(this.aborted, true, 'should be aborted');
}).on('success', function() {
test.ok(false, 'should not emit success event');
}).on('fail', function() {
test.ok(false, 'should not emit fail event');
}).abort();
},
'Should correctly hard-abort request': function(test) {
test.expect(4);
rest.get(host + '/abort').on('complete', function(data) {
test.ok(data instanceof Error, 'should be error, got: ' + util.inspect(data));
test.equal(this.aborted, true, 'should be aborted');
test.done();
}).on('error', function(err) {
test.ok(err instanceof Error, 'should be error, got: ' + util.inspect(err));
}).on('abort', function(err) {
test.equal(this.aborted, true, 'should be aborted');
}).on('success', function() {
test.ok(false, 'should not emit success event');
}).on('fail', function() {
test.ok(false, 'should not emit fail event');
}).abort(true);
},
'Should correctly handle malformed JSON': function(test) {
test.expect(4);
rest.get(host + '/mal-json').on('complete', function(data, response) {
test.ok(data instanceof Error, 'should be instanceof Error, got: ' + util.inspect(data));
test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + util.inspect(data.message));
test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + util.inspect(response.raw));
test.done();
}).on('error', function(err) {
test.ok(err instanceof Error, 'should be instanceof Error, got: ' + util.inspect(err));
}).on('success', function() {
test.ok(false, 'should not have got here');
}).on('fail', function() {
test.ok(false, 'should not have got here');
});
},
'Should correctly handle malformed XML': function(test) {
test.expect(4);
rest.get(host + '/mal-xml').on('complete', function(data, response) {
test.ok(data instanceof Error, 'should be instanceof Error, got: ' + util.inspect(data));
test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + util.inspect(data.message));
test.equal(response.raw, 'Чебурашка', 'should be "Чебурашка", got: ' + util.inspect(response.raw));
test.done();
}).on('error', function(err) {
test.ok(err instanceof Error, 'should be instanceof Error, got: ' + util.inspect(err));
}).on('success', function() {
test.ok(false, 'should not have got here');
}).on('fail', function() {
test.ok(false, 'should not have got here');
});
},
'Should correctly handle malformed YAML': function(test) {
test.expect(4);
rest.get(host + '/mal-yaml').on('complete', function(data, response) {
test.ok(data instanceof Error, 'should be instanceof Error, got: ' + util.inspect(data));
test.re(data.message, /^Failed to parse/, 'should contain "Failed to parse", got: ' + util.inspect(data.message));
test.equal(response.raw, '{Чебурашка', 'should be "{Чебурашка", got: ' + util.inspect(response.raw));
test.done();
}).on('error', function(err) {
test.ok(err instanceof Error, 'should be instanceof Error, got: ' + util.inspect(err));
}).on('success', function() {
test.ok(false, 'should not have got here');
}).on('fail', function() {
test.ok(false, 'should not have got here');
});
}
};
module.exports['Deserialization']['Should correctly convert charsets '] = function(test) {
rest.get(host + '/charset').on('complete', function(data) {
test.equal(data, 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя');
test.done();
});
};
function redirectResponse(request, response) {
if (request.url == '/redirected') {
response.writeHead(200, { 'content-type': 'text/plain' });
response.end('redirected');
} else if (request.url == '/') {
response.writeHead(301, {
'location': host + '/' + (request.headers['x-redirects'] ? '1' : 'redirected')
});
response.end('redirect');
} else {
var count = parseInt(request.url.substr(1), 10);
var max = parseInt(request.headers['x-redirects'], 10);
response.writeHead(count < max ? 301 : 200, {
'location': host + '/' + (count + 1)
});
response.end(count.toString(10));
}
}
module.exports['Redirect'] = {
setUp: setup(redirectResponse),
tearDown: teardown(),
'Should follow redirects': function(test) {
rest.get(host).on('complete', function(data) {
test.equal(data, 'redirected', 'returned: ' + util.inspect(data));
test.done();
});
},
'Should follow multiple redirects': function(test) {
rest.get(host, {
headers: { 'x-redirects': '5' }
}).on('complete', function(data) {
test.equal(data, '5', 'returned: ' + util.inspect(data));
test.done();
});
}
};
function contentLengthResponse(request, response) {
response.writeHead(200, { 'content-type': 'text/plain' });
if ('content-length' in request.headers) {
response.write(request.headers['content-length']);
} else {
response.write('content-length is not set');
}
response.end();
}
module.exports['Content-Length'] = {
setUp: setup(contentLengthResponse),
tearDown: teardown(),
'JSON content length': function(test) {
rest.post(host, {
data: JSON.stringify({ greeting: 'hello world' })
}).on('complete', function(data) {
test.equal(26, data, 'should set content-length');
test.done();
});
},
'JSON multibyte content length': function (test) {
rest.post(host, {
data: JSON.stringify({ greeting: 'こんにちは世界' })
}).on('complete', function(data) {
test.equal(36, data, 'should byte-size content-length');
test.done();
});
},
'None data request content length': function (test) {
rest.post(host).on('complete', function(data) {
test.equal(0, data, 'should set content-length');
test.done();
});
}
};
module.exports['Timeout'] = {
setUp: setup(dataResponse),
tearDown: teardown(),
'will timeout within given time': function(test) {
rest.get(host + '/timeout', {timeout: 50})
.on('timeout', function () {
test.ok(true, 'timeout endpoint is 100ms and timeout was 50ms');
test.done();
})
.on('complete', function () {
test.ok(false, 'should not emit complete event');
});
}
};