httpism
Version:
HTTP client with middleware and good defaults
1,081 lines (940 loc) • 33.3 kB
JavaScript
var httpism = require("../index");
var express = require("express");
var bodyParser = require("body-parser");
var chai = require("chai");
chai.use(require("chai-as-promised"));
var assert = chai.assert;
var expect = chai.expect;
chai.should();
var http = require('http');
var https = require("https");
var fs = require("fs-promise");
var qs = require("qs");
var middleware = require("../middleware");
var basicAuth = require("basic-auth-connect");
var cookieParser = require("cookie-parser");
var toughCookie = require("tough-cookie");
var httpProxy = require('http-proxy');
var net = require('net');
describe("httpism", function() {
var server;
var app;
var port = 12345;
var baseurl = "http://localhost:" + port;
beforeEach(function() {
app = express();
server = app.listen(port);
});
afterEach(function() {
server.close();
});
describe("json", function() {
beforeEach(function() {
app.use(bodyParser.json());
});
function itCanMakeRequests(method) {
it("can make " + method + " requests", function() {
app[method.toLowerCase()]("/", function(req, res) {
res.send({
method: req.method,
path: req.path,
accept: req.headers.accept
});
});
return httpism[method.toLowerCase()](baseurl).then(function(response) {
expect(response.body).to.eql({
method: method,
path: "/",
accept: "application/json"
});
});
});
}
it("can make HEAD requests", function() {
app.head("/", function(req, res) {
res.header("x-method", req.method);
res.header("x-path", req.path);
res.end();
});
return httpism.head(baseurl).then(function(response) {
response.headers["x-method"].should.equal("HEAD");
response.headers["x-path"].should.equal("/");
});
});
function itCanMakeRequestsWithBody(method) {
it("can make " + method + " requests with body", function() {
app[method.toLowerCase()]("/", function(req, res) {
res.send({
method: req.method,
path: req.path,
accept: req.headers.accept,
body: req.body
});
});
return httpism[method.toLowerCase()](baseurl, {
joke: "a chicken..."
}).then(function(response) {
response.body.should.eql({
method: method,
path: "/",
accept: "application/json",
body: {
joke: "a chicken..."
}
});
});
});
}
itCanMakeRequests("GET");
itCanMakeRequests("DELETE");
itCanMakeRequestsWithBody("POST");
itCanMakeRequestsWithBody("PUT");
itCanMakeRequestsWithBody("PATCH");
itCanMakeRequestsWithBody("OPTIONS");
describe("content type request header", function() {
beforeEach(function() {
app.post("/", function(req, res) {
res.header("received-content-type", req.headers["content-type"]);
res.header("content-type", "text/plain");
req.pipe(res);
});
});
it("can upload JSON as application/custom", function() {
return httpism.post(baseurl, { json: "json" }, { headers: { "content-type": "application/custom" } }).then(function(response) {
JSON.parse(response.body).should.eql({
json: "json"
});
expect(response.headers["received-content-type"]).to.eql("application/custom");
});
});
it("can upload form as application/custom", function() {
return httpism.post(baseurl, { json: "json" }, {
form: true,
headers: {
"content-type": "application/custom"
}
}).then(function(response) {
qs.parse(response.body).should.eql({
json: "json"
});
response.headers["received-content-type"].should.eql("application/custom");
});
});
it("can upload string as application/custom", function() {
return httpism.post(baseurl, "a string", {
headers: {
"content-type": "application/custom"
}
}).then(function(response) {
response.body.should.eql("a string");
response.headers["received-content-type"].should.eql("application/custom");
});
});
});
describe("content-length header", function() {
var unicodeText = "♫♫♫♫♪ ☺";
beforeEach(function() {
return app.post("/", function(req, res) {
res.send({
"content-length": req.headers["content-length"],
"transfer-encoding": req.headers["transfer-encoding"]
});
});
});
it("sends content-length, and not transfer-encoding: chunked, with JSON", function() {
return httpism.post(baseurl, { json: unicodeText }).then(function(response) {
response.body.should.eql({
"content-length": Buffer.byteLength(JSON.stringify({
json: unicodeText
})).toString()
});
});
});
it("sends content-length, and not transfer-encoding: chunked, with plain text", function() {
return httpism.post(baseurl, unicodeText).then(function(response) {
response.body.should.eql({
"content-length": Buffer.byteLength(unicodeText).toString()
});
});
});
it("sends content-length, and not transfer-encoding: chunked, with form data", function() {
return httpism.post(baseurl, { formData: unicodeText }, {
form: true
}).then(function(response) {
response.body.should.eql({
"content-length": Buffer.byteLength(qs.stringify({
formData: unicodeText
})).toString()
});
});
});
});
describe("accept request header", function() {
beforeEach(function() {
app.get("/", function(req, res) {
res.header("content-type", "text/plain");
res.send(req.headers.accept);
});
});
it("sends Accept: application/json by default", function() {
return httpism.get(baseurl).then(function(response) {
response.body.should.eql("application/json");
});
});
it("can send a custom Accept header", function() {
return httpism.get(baseurl, {
headers: {
accept: "application/custom"
}
}).then(function(response) {
response.body.should.eql("application/custom");
});
});
});
describe("request headers", function() {
it("can specify headers for the request", function() {
app.get("/", function(req, res) {
res.send({
"x-header": req.headers["x-header"]
});
});
return httpism.get(baseurl, {
headers: {
"x-header": "haha"
}
}).then(function(response) {
response.body["x-header"].should.equal("haha");
});
});
});
describe("text", function() {
function itReturnsAStringForContentType(mimeType) {
it("returns a string if the content-type is " + mimeType, function() {
app.get("/", function(req, res) {
res.header("content-type", mimeType);
res.send("content as string");
});
return httpism.get(baseurl).then(function(response) {
response.body.should.equal("content as string");
});
});
}
itReturnsAStringForContentType("text/plain");
itReturnsAStringForContentType("text/html");
itReturnsAStringForContentType("text/css");
itReturnsAStringForContentType("text/javascript");
itReturnsAStringForContentType("application/javascript");
it("will upload a string as text/plain", function() {
app.post("/text", function(req, res) {
res.header("received-content-type", req.headers["content-type"]);
res.header("content-type", "text/plain");
req.pipe(res);
});
return httpism.post(baseurl + "/text", "content as string").then(function(response) {
response.headers["received-content-type"].should.equal("text/plain");
response.body.should.equal("content as string");
});
});
});
describe("query strings", function() {
beforeEach(function() {
app.get("/", function(req, res) {
res.send(req.query);
});
});
it("can set query string", function() {
return httpism.get(baseurl, {
querystring: {
a: "a",
b: "b"
}
}).then(function(response) {
response.body.should.eql({
a: "a",
b: "b"
});
});
});
it("can override query string in url", function() {
return httpism.get(baseurl + "/?a=a&c=c", {
querystring: {
a: "newa",
b: "b"
}
}).then(function(response) {
response.body.should.eql({
a: "newa",
b: "b",
c: "c"
});
});
});
});
describe("apis", function() {
it("can make a new client that adds headers", function() {
app.get("/", function(req, res) {
res.send({
joke: req.headers.joke
});
});
var client = httpism.api(function(request, next) {
request.headers.joke = "a chicken...";
return next(request);
});
return client.get(baseurl).then(function(response) {
response.body.should.eql({
joke: "a chicken..."
});
});
});
describe('cache example', function () {
var filename = __dirname + "/cachefile.txt";
beforeEach(function() {
return fs.writeFile(filename, '{"from": "cache"}');
});
afterEach(function() {
return fs.unlink(filename);
});
it('can insert a new middleware just before the http request, dealing with streams', function () {
app.get('/', function (req, res) {
res.send({from: 'server'});
});
var cache = function (req, next) {
return next().then(function (response) {
response.body = fs.createReadStream(filename);
return response;
});
};
cache.before = 'http';
var http = httpism.api(cache);
return http.get(baseurl).then(function (response) {
expect(response.body).to.eql({from: 'cache'});
});
});
});
it('can insert middleware before another', function () {
var m = function () {};
m.before = 'http';
var api = httpism.raw.api(m);
expect(api.middlewares).to.eql([
m,
middleware.http
]);
});
it('can insert middleware after another', function () {
var m = function () {};
m.after = 'http';
var api = httpism.raw.api(m);
expect(api.middlewares).to.eql([
middleware.http,
m
]);
});
it('throws if before middleware name cannot be found', function () {
var m = function () {};
m.before = 'notfound';
expect(function () { httpism.api(m); }).to.throw('no such middleware: notfound');
});
it('throws if after middleware name cannot be found', function () {
var m = function () {};
m.after = 'notfound';
expect(function () { httpism.api(m); }).to.throw('no such middleware: notfound');
});
});
describe("exceptions", function() {
beforeEach(function() {
app.get("/400", function(req, res) {
res.status(400).send({
message: "oh dear"
});
});
});
it("throws exceptions on 400-500 status codes, by default", function() {
return httpism.api(baseurl).get("/400").then(function () {
assert.fail("expected an exception to be thrown");
}).catch(function(e) {
e.message.should.equal("GET " + baseurl + "/400 => 400 Bad Request");
e.statusCode.should.equal(400);
e.body.message.should.equal("oh dear");
});
});
it("doesn't throw exceptions on 400-500 status codes, when specified", function() {
return httpism.api(baseurl).get("/400", { exceptions: false }).then(function(response) {
response.body.message.should.equal("oh dear");
});
});
it("throws if it cannot connect", function() {
return expect(httpism.get("http://localhost:4001/")).to.eventually.be.rejectedWith("ECONNREFUSED");
});
});
describe("options", function() {
var client;
beforeEach(function() {
client = httpism.api(function(request, next) {
request.body = request.options;
return next(request);
}, { a: "a" });
app.post("/", function(req, res) {
res.send(req.body);
});
});
it("clients have options, which can be overwritten on each request", function() {
var root = client.api(baseurl);
return root.post("", undefined, { b: "b" }).then(function(response) {
response.body.should.eql({
a: "a",
b: "b"
});
return response.post("", undefined, { c: "c" }).then(function(response) {
response.body.should.eql({
a: "a",
c: "c"
});
return root.post("", undefined).then(function(response) {
return response.body.should.eql({
a: "a"
});
});
});
});
});
});
describe("responses act as clients", function() {
var path;
beforeEach(function() {
function pathResponse(req, res) {
res.send({
path: req.path
});
}
app.get("/", pathResponse);
app.get("/rootfile", pathResponse);
app.get("/path/", pathResponse);
app.get("/path/file", pathResponse);
var api = httpism.api(baseurl);
return api.get("/path/").then(function(response) {
path = response;
});
});
it("resources respond with their url", function() {
path.url.should.equal(baseurl + "/path/");
path.body.path.should.equal("/path/");
});
it("addresses original resource if url is ''", function() {
return path.get("").then(function(response) {
response.body.path.should.equal("/path/");
});
});
it("makes relative sub path", function() {
return path.get("file").then(function(response) {
response.body.path.should.equal("/path/file");
});
});
it("addresses root", function() {
return path.get("/").then(function(response) {
response.body.path.should.equal("/");
});
});
it("can address ../ paths", function() {
return path.get("../rootfile").then(function(response) {
response.body.path.should.equal("/rootfile");
});
});
it("can create new apis from relative paths", function() {
return path.api("file").get("").then(function(response) {
response.body.path.should.equal("/path/file");
});
});
});
describe("redirects", function() {
beforeEach(function() {
app.get("/redirecttoredirect", function(req, res) {
res.redirect("/redirect");
});
app.get("/redirect", function(req, res) {
res.location("/path/");
res.status(302).send({
path: req.path
});
});
app.get("/", function(req, res) {
res.send({
path: req.path
});
});
app.get("/path/", function(req, res) {
res.send({
path: req.path
});
});
app.get("/path/file", function(req, res) {
res.send({
path: req.path
});
});
});
it("follows redirects by default", function() {
return httpism.get(baseurl + "/redirect").then(function(response) {
response.body.should.eql({
path: "/path/"
});
response.url.should.eql(baseurl + "/path/");
});
});
function itFollowsRedirects(statusCode) {
it("follows " + statusCode + " redirects", function() {
app.get("/" + statusCode, function(req, res) {
res.location("/path/");
res.status(statusCode).send();
});
return httpism.get(baseurl + "/" + statusCode).then(function(response) {
response.body.should.eql({
path: "/path/"
});
response.url.should.eql(baseurl + "/path/");
});
});
}
describe("redirects", function() {
itFollowsRedirects(300);
itFollowsRedirects(301);
itFollowsRedirects(302);
itFollowsRedirects(303);
itFollowsRedirects(307);
});
it("paths are relative to destination resource", function() {
return httpism.get(baseurl + "/redirect").then(function(response) {
return response.get("file").then(function(response) {
response.body.path.should.equal("/path/file");
});
});
});
it("follows a more than one redirect", function() {
return httpism.get(baseurl + "/redirecttoredirect").then(function(response) {
response.body.should.eql({
path: "/path/"
});
response.url.should.eql(baseurl + "/path/");
});
});
it("doesn't follow redirects when specified", function() {
return httpism.get(baseurl + "/redirect", {
redirect: false
}).then(function(response) {
response.body.should.eql({
path: "/redirect"
});
response.url.should.eql(baseurl + "/redirect");
response.headers.location.should.equal("/path/");
response.statusCode.should.equal(302);
});
});
});
describe("cookies", function() {
beforeEach(function() {
app.use(cookieParser());
app.get("/setcookie", function(req, res) {
res.cookie("mycookie", "value");
res.send({});
});
app.get("/getcookie", function(req, res) {
res.send(req.cookies);
});
});
it("can store cookies and send cookies", function() {
var cookies = new toughCookie.CookieJar();
return httpism.get(baseurl + "/setcookie", {
cookies: cookies
}).then(function() {
return httpism.get(baseurl + "/getcookie", {
cookies: cookies
}).then(function(response) {
response.body.should.eql({
mycookie: "value"
});
});
});
});
it("can store cookies and send cookies", function() {
var api = httpism.api(baseurl, {
cookies: true
});
return api.get(baseurl + "/setcookie").then(function() {
return api.get(baseurl + "/getcookie").then(function(response) {
response.body.should.eql({
mycookie: "value"
});
});
});
});
});
describe("https", function() {
var httpsServer;
var httpsPort = 23456;
var httpsBaseurl = "https://localhost:" + httpsPort + "/";
beforeEach(function() {
var credentials = {
key: fs.readFileSync(__dirname + "/server.key", "utf-8"),
cert: fs.readFileSync(__dirname + "/server.crt", "utf-8")
};
httpsServer = https.createServer(credentials, app);
httpsServer.listen(httpsPort);
});
afterEach(function() {
httpsServer.close();
});
it("can make HTTPS requests", function() {
app.get("/", function(req, res) {
res.send({
protocol: req.protocol
});
});
return httpism.get(httpsBaseurl, { https: { rejectUnauthorized: false } }).then(function(response) {
response.body.protocol.should.equal("https");
});
});
});
describe("forms", function() {
it("can upload application/x-www-form-urlencoded", function() {
app.post("/form", function(req, res) {
res.header("content-type", "text/plain");
res.header("received-content-type", req.headers["content-type"]);
req.pipe(res);
});
return httpism.post(baseurl + "/form", {
name: "Betty Boo",
address: "one & two"
}, {
form: true
}).then(function(response) {
response.body.should.equal("name=Betty%20Boo&address=one%20%26%20two");
response.headers["received-content-type"].should.equal("application/x-www-form-urlencoded");
});
});
it("can download application/x-www-form-urlencoded", function() {
app.get("/form", function(req, res) {
res.header("content-type", "application/x-www-form-urlencoded");
res.send(qs.stringify({
name: "Betty Boo",
address: "one & two"
}));
});
return httpism.get(baseurl + "/form").then(function(response) {
response.body.should.eql({
name: "Betty Boo",
address: "one & two"
});
response.headers["content-type"].should.equal("application/x-www-form-urlencoded; charset=utf-8");
});
});
});
describe("basic authentication", function() {
beforeEach(function() {
app.use(basicAuth(function(user, pass) {
return user === "good user" && pass === "good password!";
}));
return app.get("/secret", function(req, res) {
res.send("this is secret");
});
});
it("can authenticate using username password", function() {
return httpism.get(baseurl + "/secret", {
basicAuth: {
username: "good user",
password: "good password!"
}
}).then(function(response) {
response.body.should.equal("this is secret");
});
});
it("can authenticate using username password encoded in URL", function() {
var u = encodeURIComponent;
return httpism.get("http://" + u("good user") + ":" + u("good password!") + "@localhost:" + port + "/secret").then(function(response) {
response.body.should.equal("this is secret");
});
});
it("can authenticate using username with colons :", function() {
return httpism.get(baseurl + "/secret", {
basicAuth: {
username: "good: :user",
password: "good password!"
}
}).then(function(response) {
response.body.should.equal("this is secret");
});
});
it("fails to authenticate when password is incorrect", function() {
return httpism.get(baseurl + "/secret", {
basicAuth: {
username: "good user",
password: "bad password!"
},
exceptions: false
}).then(function(response) {
response.statusCode.should.equal(401);
});
});
});
});
describe("streams", function() {
var filename = __dirname + "/afile.txt";
beforeEach(function() {
return fs.writeFile(filename, "some content").then(function () {
app.post("/file", function(req, res) {
res.header("content-type", "text/plain");
res.header("received-content-type", req.headers["content-type"]);
req.unshift("received: ");
req.pipe(res);
});
app.get("/file", function(req, res) {
var stream;
stream = fs.createReadStream(filename);
res.header("content-type", "application/blah");
stream.pipe(res);
});
});
});
afterEach(function() {
return fs.unlink(filename);
});
function itCanUploadAStreamWithContentType(contentType) {
it("can upload a stream with Content-Type: " + contentType, function() {
var stream = fs.createReadStream(filename);
return httpism.post(baseurl + "/file", stream, {
headers: {
"content-type": contentType
}
}).then(function(response) {
response.headers["received-content-type"].should.equal(contentType);
response.body.should.equal("received: some content");
});
});
}
itCanUploadAStreamWithContentType("application/blah");
itCanUploadAStreamWithContentType("application/json");
itCanUploadAStreamWithContentType("text/plain");
itCanUploadAStreamWithContentType("application/x-www-form-urlencoded");
it("can download a stream", function() {
return httpism.get(baseurl + "/file").then(function(response) {
response.headers["content-type"].should.equal("application/blah");
return middleware.streamToString(response.body).then(function(response) {
response.should.equal("some content");
});
});
});
describe("forcing response parsing", function() {
function describeForcingResponse(type, options) {
var contentType = options !== undefined && Object.prototype.hasOwnProperty.call(options, "contentType") && options.contentType !== undefined ? options.contentType : undefined;
var content = options !== undefined && Object.prototype.hasOwnProperty.call(options, "content") && options.content !== undefined ? options.content : undefined;
var sendContent = options !== undefined && Object.prototype.hasOwnProperty.call(options, "sendContent") && options.sendContent !== undefined ? options.sendContent : undefined;
describe(type, function() {
it("can download a stream of content-type " + contentType, function() {
app.get("/content", function(req, res) {
var stream = fs.createReadStream(filename);
res.header("content-type", contentType);
stream.pipe(res);
});
return httpism.get(baseurl + "/content", {
responseBody: "stream"
}).then(function(response) {
response.headers["content-type"].should.equal(contentType);
return middleware.streamToString(response.body).then(function(response) {
response.should.equal("some content");
});
});
});
it("can force parse " + type + " when content-type is application/blah", function() {
app.get("/content", function(req, res) {
res.header("content-type", "application/blah");
res.send(sendContent || content);
});
return httpism.get(baseurl + "/content", {
responseBody: type
}).then(function(response) {
response.headers["content-type"].should.equal("application/blah; charset=utf-8");
response.body.should.eql(content);
});
});
});
}
describeForcingResponse("text", {
contentType: "text/plain; charset=utf-8",
content: "some text content"
});
describeForcingResponse("json", {
contentType: "application/json",
content: {
json: true
}
});
describeForcingResponse("form", {
contentType: "application/x-www-form-urlencoded",
content: {
json: "true"
},
sendContent: qs.stringify({
json: "true"
})
});
});
});
describe('proxy', function () {
var proxyServer;
var proxyPort = 12346;
var proxy;
var urlProxied;
var proxyAuth = false;
var proxyUrl = 'http://localhost:' + proxyPort + '/';
var secureProxyUrl = 'http://bob:secret@localhost:' + proxyPort + '/';
function proxyRequest(req, res) {
urlProxied = req.url;
proxy.web(req, res, { target: req.url });
}
function checkProxyAuthentication(req, res, next) {
var expectedAuthorisation = 'Basic ' + new Buffer('bob:secret').toString('base64');
if (expectedAuthorisation == req.headers['proxy-authorization']) {
next(req, res);
} else {
res.statusCode = 407;
res.end('bad proxy authentication');
}
}
beforeEach(function() {
urlProxied = undefined;
proxy = httpProxy.createProxyServer();
proxyServer = http.createServer(function (req, res) {
if (proxyAuth) {
return checkProxyAuthentication(req, res, proxyRequest);
} else {
return proxyRequest(req, res);
}
});
proxyServer.listen(proxyPort);
proxyServer.on('connect', function(req, socket) {
var addr = req.url.split(':');
//creating TCP connection to remote server
var conn = net.connect(addr[1] || 443, addr[0], function() {
// tell the client that the connection is established
socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', function() {
// creating pipes in both ends
conn.pipe(socket);
socket.pipe(conn);
});
});
conn.on('error', function(e) {
console.log("Server connection error: " + e, addr);
socket.end();
});
});
});
afterEach(function() {
proxyServer.close();
});
var httpsServer;
var httpsPort = 23456;
var httpsBaseurl = "https://localhost:" + httpsPort + "/";
beforeEach(function() {
var credentials = {
key: fs.readFileSync(__dirname + "/server.key", "utf-8"),
cert: fs.readFileSync(__dirname + "/server.crt", "utf-8")
};
httpsServer = https.createServer(credentials, app);
httpsServer.listen(httpsPort);
});
afterEach(function() {
httpsServer.close();
});
context('unsecured proxy', function () {
it('can use a proxy', function () {
app.get("/", function(req, res) {
res.send({
blah: "blah"
});
});
return httpism.get(baseurl, {proxy: proxyUrl}).then(function (response) {
expect(response.body).to.eql({blah: 'blah'});
expect(urlProxied).to.equal(baseurl);
});
});
it("can make HTTPS requests", function() {
app.get("/", function(req, res) {
res.send({
protocol: req.protocol
});
});
return httpism.get(httpsBaseurl, { proxy: proxyUrl, https: { rejectUnauthorized: false } }).then(function(response) {
response.body.protocol.should.equal("https");
});
});
});
context('secured proxy', function () {
it('can use a proxy', function () {
app.get("/", function(req, res) {
res.send({
blah: "blah"
});
});
return httpism.get(baseurl, {proxy: secureProxyUrl}).then(function (response) {
expect(response.body).to.eql({blah: 'blah'});
expect(urlProxied).to.equal(baseurl);
});
});
it("can make HTTPS requests", function() {
app.get("/", function(req, res) {
res.send({
protocol: req.protocol
});
});
return httpism.get(httpsBaseurl, { proxy: secureProxyUrl, https: { rejectUnauthorized: false } }).then(function(response) {
response.body.protocol.should.equal("https");
});
});
});
});
describe("raw", function() {
it("can be used to create new middleware pipelines", function() {
app.get("/", function(req, res) {
res.status(400).send({
blah: "blah"
});
});
var api = httpism.raw.api(baseurl, function(request, next) {
return next().then(function(res) {
return middleware.streamToString(res.body).then(function(response) {
res.body = response;
return res;
});
});
});
return api.get(baseurl).then(function(response) {
response.statusCode.should.equal(400);
JSON.parse(response.body).should.eql({
blah: "blah"
});
});
});
});
describe("json reviver", function() {
it("controls how the JSON response is deserialised", function() {
app.get("/", function(req, res) {
res.status(200).send({ blah: 1234 });
});
var api = httpism.api(baseurl, {
jsonReviver: function(key, value) {
if (key == '') { return value; }
return key + value + '!';
}
});
return api.get(baseurl).then(function(response) {
response.statusCode.should.equal(200);
response.body.should.eql({
blah: "blah1234!"
});
});
});
});
describe('use', function () {
it('can add a middleware to the outside', function () {
});
});
});