UNPKG

httpism

Version:

HTTP client with middleware and good defaults

1,635 lines (1,413 loc) 56.5 kB
/* eslint-env mocha */ 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 var http = require('http') var https = require('https') var fs = require('fs-promise') var qs = require('qs') var streamToString = require('../streamToString') var basicAuthConnect = require('basic-auth-connect') var basicAuth = require('basic-auth') var cookieParser = require('cookie-parser') var toughCookie = require('tough-cookie') var httpProxy = require('http-proxy') var net = require('net') var FormData = require('form-data') var multiparty = require('multiparty') var obfuscateUrlPassword = require('../obfuscateUrlPassword') var urlTemplate = require('url-template') var pathUtils = require('path') var cache = require('../middleware/cache') 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 (body) { expect(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) { expect(response.headers['x-method']).to.equal('HEAD') expect(response.headers['x-path']).to.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 (body) { expect(body).to.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' }, { response: true, headers: { 'content-type': 'application/custom' } }).then(function (response) { expect(JSON.parse(response.body)).to.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' }, { response: true, form: true, headers: { 'content-type': 'application/custom' } }).then(function (response) { expect(qs.parse(response.body)).to.eql({ json: 'json' }) expect(response.headers['received-content-type']).to.eql('application/custom') }) }) it('can upload string as application/custom', function () { return httpism.post(baseurl, 'a string', { response: true, headers: { 'content-type': 'application/custom' } }).then(function (response) { expect(response.body).to.eql('a string') expect(response.headers['received-content-type']).to.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 (body) { expect(body).to.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 (body) { expect(body).to.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) { expect(response).to.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 (body) { expect(body).to.eql('application/json') }) }) it('can send a custom Accept header', function () { return httpism.get(baseurl, { headers: { accept: 'application/custom' } }).then(function (body) { expect(body).to.eql('application/custom') }) }) it('can send a custom Accept header even mixed case', function () { return httpism.get(baseurl, { headers: { Accept: 'application/custom' } }).then(function (body) { expect(body).to.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 (body) { expect(body['x-header']).to.equal('haha') }) }) it('headers are not mutated during the request', function () { app.post('/', function (req, res) { res.send({ 'x-header': req.headers['x-header'] }) }) var headers = { 'x-header': 'haha' } return httpism.post(baseurl, { body: 'my body' }, { headers: headers }).then(function (body) { expect(headers).to.eql({ 'x-header': '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 (body) { expect(body).to.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', { response: true }).then(function (response) { expect(response.headers['received-content-type']).to.equal('text/plain') expect(response.body).to.equal('content as string') }) }) }) describe('params', function () { beforeEach(function () { app.get('/auth', function (req, res) { res.send(basicAuth(req) || {}) }) app.get('*', function (req, res) { res.send(req.url) }) }) it('can set params', function () { return httpism.get(baseurl + '/:a', { params: { a: 'aa', b: 'bb' } }).then(function (body) { expect(body).to.eql('/aa?b=bb') }) }) it('can set path params', function () { return httpism.get(baseurl + '/:a*/:b', { params: { a: 'a/long/path', b: 'bb' } }).then(function (body) { expect(body).to.eql('/a/long/path/bb') }) }) it("doesn't replace credentials", function () { return httpism.get('http://user:pass@localhost:' + port + '/auth', { params: { a: 'a/long/path', b: 'bb' } }).then(function (body) { expect(body).to.eql({ name: 'user', pass: 'pass' }) }) }) it('uses escapes', function () { return httpism.get(baseurl + '/:a/:b', { params: { a: 'a/a', b: 'b/b', c: 'c/c' } }).then(function (body) { expect(body).to.eql('/a%2Fa/b%2Fb?c=c%2Fc') }) }) it('can use other template engines', function () { return httpism.get(baseurl + '/{a}{?b,c}', { params: { a: 'x', b: 'y', c: 'z' }, expandUrl: function (url, params) { var template = urlTemplate.parse(url) return template.expand(params) } }).then(function (body) { expect(body).to.eql('/x?b=y&c=z') }) }) }) describe('.client()', function () { describe('options', 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.client(function (request, next) { request.headers.joke = 'a chicken...' return next(request) }) return client.get(baseurl).then(function (body) { expect(body).to.eql({ joke: 'a chicken...' }) }) }) it('can make a new client that adds headers by passing them to options', function () { app.get('/', function (req, res) { res.send({ x: req.headers.x, y: req.headers.y }) }) var client = httpism.client({ headers: { x: '123' } }).client({ headers: { y: '456' } }) return client.get(baseurl).then(function (body) { expect(body).to.eql({ x: '123', y: '456' }) }) }) it('can specify options in client, and override with another client', function () { app.get('/', function (req, res) { res.send({ x: req.query.x, y: req.query.y }) }) var client = httpism .client({ params: { x: 'original x', y: 'original y' } }) .client({ params: { y: 'new y' } }) return client.get(baseurl).then(function (body) { expect(body).to.eql({ x: 'original x', y: 'new y' }) }) }) it('can specify options in client, and override them when making request', function () { app.get('/', function (req, res) { res.send({ x: req.query.x, y: req.query.y }) }) var client = httpism .client({ params: { x: 'original x', y: 'original y' } }) return client.get(baseurl, { params: { y: 'new y' } }).then(function (body) { expect(body).to.eql({ x: 'original x', y: 'new y' }) }) }) it('makes requests with additional headers', function () { app.get('/', function (req, res) { res.send({ x: req.headers.x, y: req.headers.y }) }) var client = httpism.client({ headers: { x: '123' } }) return client.get(baseurl, { headers: { y: '456' } }).then(function (body) { expect(body).to.eql({ x: '123', y: '456' }) }) }) }) describe('cache example', function () { var filename = pathUtils.join(__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.httpismMiddleware = { before: 'http' } var http = httpism.client(cache) return http.get(baseurl).then(function (body) { expect(body).to.eql({ from: 'cache' }) }) }) }) describe('middleware', function () { describe('defining middleware', function () { var client beforeEach(function () { app.get('/', function (req, res) { res.send(req.query) }) client = httpism.client(baseurl) }) it('can modify the request', function () { client.use(function (req, next) { req.url += '?param=hi' return next() }) return client.get('/').then(function (response) { expect(response).to.eql({ param: 'hi' }) }) }) it('can send an entirely new request', function () { client.use(function (req, next) { return next({ url: req.url + '?param=hi', method: 'get', options: {}, headers: {} }) }) return client.get('/').then(function (response) { expect(response).to.eql({ param: 'hi' }) }) }) it('can return an entirely new response', function () { client.use(function (req, next) { return next().then(function (response) { return { body: 'body' } }) }) return client.get('/').then(function (response) { expect(response).to.eql('body') }) }) it('can do its own parsing and respond with a frozen body', function () { client.use(function (req, next) { return next().then(function (response) { var body = { ice: 'cold' } Object.freeze(body) return { body: body } }) }) return client.get('/', { responseBody: 'text' }).then(function (response) { expect(response.ice).to.eql('cold') }) }) }) describe('inserting middleware', function () { var pipeline, a, b function middlwareNamed (name) { function middleware () { } middleware.httpismMiddleware = { name: name } return middleware } beforeEach(function () { pipeline = httpism.raw.client() pipeline.middleware = [ a = middlwareNamed('a'), b = middlwareNamed('b') ] }) describe('before', function () { it('can insert middleware before another', function () { var m = function () {} m.httpismMiddleware = { before: 'b' } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, m, b ]) }) it('can insert middleware into same client before another', function () { var m = function () {} m.httpismMiddleware = { before: 'b' } pipeline.use(m) expect(pipeline.middleware).to.eql([ a, m, b ]) }) it('inserts before the named middleware if at least one is found', function () { var m = function () {} m.httpismMiddleware = { before: ['b', 'c'] } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, m, b ]) }) it('inserts before all the named middleware if all are found', function () { var m = function () {} m.httpismMiddleware = { before: ['b', 'b'] } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, m, b ]) }) }) describe('after', function () { it('can insert middleware after another', function () { var m = function () {} m.httpismMiddleware = { after: 'a' } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, m, b ]) }) it('inserts after the named middleware if at lesat one is found', function () { var m = function () {} m.httpismMiddleware = { after: ['a', 'c'] } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, m, b ]) }) it('inserts after all the named middleware if all are found', function () { var m = function () {} m.httpismMiddleware = { after: ['a', 'b'] } var client = pipeline.client(m) expect(client.middleware).to.eql([ a, b, m ]) }) }) it('can remove middleware', function () { pipeline.remove('b') expect(pipeline.middleware).to.eql([ a ]) }) it('throws if before middleware name cannot be found', function () { var m = function () {} m.httpismMiddleware = { before: 'notfound' } expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound') }) it('throws if none of the before middleware names can be found', function () { var m = function () {} m.httpismMiddleware = { before: ['notfound'] } expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound') }) it('throws if after middleware name cannot be found', function () { var m = function () {} m.httpismMiddleware = { after: 'notfound' } expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound') }) it('throws if none of the after middleware names can be found', function () { var m = function () {} m.httpismMiddleware = { after: ['notfound'] } expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound') }) }) }) describe('2.x compatibility', function () { it('can be created using `.api()`', 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 (body) { expect(body).to.eql({ joke: 'a chicken...' }) }) }) }) describe('query strings (deprecated)', 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 (body) { expect(body).to.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 (body) { expect(body).to.eql({ a: 'newa', b: 'b', c: 'c' }) }) }) }) }) 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.client(baseurl).get('/400').then(function () { assert.fail('expected an exception to be thrown') }).catch(function (e) { expect(e.message).to.equal('GET ' + baseurl + '/400 => 400 Bad Request') expect(e.statusCode).to.equal(400) expect(e.body.message).to.equal('oh dear') }) }) it("doesn't include the password in the error message", function () { return httpism.client('http://user:pass@localhost:' + port + '/').get('/400').then(function () { assert.fail('expected an exception to be thrown') }).catch(function (e) { expect(e.message).to.equal('GET http://user:********@localhost:' + port + '/400 => 400 Bad Request') expect(e.url).to.equal('http://user:********@localhost:' + port + '/400') expect(e.statusCode).to.equal(400) expect(e.body.message).to.equal('oh dear') }) }) it("doesn't throw exceptions on 400-500 status codes, when specified", function () { return httpism.client(baseurl).get('/400', { exceptions: false }).then(function (body) { expect(body.message).to.equal('oh dear') }) }) describe('error predicate', function () { it('throws exceptions when predicate returns true', function () { function isError (response) { return response.statusCode === 400 } return httpism.client(baseurl).get('/400', { exceptions: isError }).then(function () { assert.fail('expected an exception to be thrown') }).catch(function (e) { expect(e.message).to.equal('GET ' + baseurl + '/400 => 400 Bad Request') expect(e.statusCode).to.equal(400) expect(e.body.message).to.equal('oh dear') }) }) it("doesn't throw exceptions when predicate returns false", function () { function isError (response) { return response.statusCode !== 400 } return httpism.client(baseurl).get('/400', { exceptions: isError }).then(function (body) { expect(body.message).to.equal('oh dear') }) }) }) it('throws if it cannot connect', function () { return expect(httpism.get('http://localhost:50000/')).to.eventually.be.rejectedWith('ECONNREFUSED') }) }) describe('options', function () { var client beforeEach(function () { client = httpism.client(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.client(baseurl) return root.post('', undefined, { b: 'b' }).then(function (body) { expect(body).to.eql({ a: 'a', b: 'b' }) return root.client().post('', undefined, { c: 'c' }).then(function (body) { expect(body).to.eql({ a: 'a', c: 'c' }) return root.post('', undefined).then(function (body) { expect(body).eql({ a: 'a' }) }) }) }) }) }) describe('responses', function () { beforeEach(function () { app.get('/', function (req, res) { res.set({ 'x-custom-header': 'header value' }) res.status(234) res.send({ data: 'data' }) }) }) describe('response: true', function () { var response beforeEach(function () { return httpism.get(baseurl, { response: true }).then(function (_response) { response = _response }) }) it('contains the url', function () { expect(response.url).to.equal(baseurl) }) it('contains the status code', function () { expect(response.statusCode).to.equal(234) }) it('contains the headers', function () { expect(response.headers['x-custom-header']).to.equal('header value') }) it('contains the body', function () { expect(response.body).to.eql({ data: 'data' }) }) }) describe('response: false (default)', function () { var body beforeEach(function () { return httpism.get(baseurl).then(function (_body) { body = _body }) }) describe('2.x compatibility', function () { it('contains the url', function () { expect(body.url).to.equal(baseurl) }) it('contains the status code', function () { expect(body.statusCode).to.equal(234) }) it('contains the headers', function () { expect(body.headers['x-custom-header']).to.equal('header value') }) it('contains the body', function () { expect(body.body).to.eql({ data: 'data' }) }) }) it('returns the body', function () { expect(body).to.eql({ data: 'data' }) }) }) }) 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', { response: true }).then(function (response) { expect(response.body).to.eql({ path: '/path/' }) expect(response.url).to.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, { response: true }).then(function (response) { expect(response.body).to.eql({ path: '/path/' }) expect(response.url).to.eql(baseurl + '/path/') }) }) } describe('redirects', function () { itFollowsRedirects(300) itFollowsRedirects(301) itFollowsRedirects(302) itFollowsRedirects(303) itFollowsRedirects(307) }) it('follows a more than one redirect', function () { return httpism.get(baseurl + '/redirecttoredirect', { response: true }).then(function (response) { expect(response.body).to.eql({ path: '/path/' }) expect(response.url).to.eql(baseurl + '/path/') }) }) it("doesn't follow redirects when specified", function () { return httpism.get(baseurl + '/redirect', { redirect: false, response: true }).then(function (response) { expect(response.body).to.eql({ path: '/redirect' }) expect(response.url).to.eql(baseurl + '/redirect') expect(response.headers.location).to.equal('/path/') expect(response.statusCode).to.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 (body) { expect(body).to.eql({ mycookie: 'value' }) }) }) }) it('can store cookies and send cookies', function () { var client = httpism.client(baseurl, { cookies: true }) return client.get(baseurl + '/setcookie').then(function () { return client.get(baseurl + '/getcookie').then(function (body) { expect(body).to.eql({ mycookie: 'value' }) }) }) }) }) describe('https', function () { var httpsServer var httpsPort = 23456 var httpsBaseurl = 'https://localhost:' + httpsPort + '/' beforeEach(function () { var credentials = { key: fs.readFileSync(pathUtils.join(__dirname, 'server.key'), 'utf-8'), cert: fs.readFileSync(pathUtils.join(__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 (body) { expect(body.protocol).to.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 Boop', address: 'one & two' }, { form: true, response: true }).then(function (response) { expect(response.body).to.equal('name=Betty%20Boop&address=one%20%26%20two') expect(response.headers['received-content-type']).to.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 Boop', address: 'one & two' })) }) return httpism.get(baseurl + '/form', { response: true }).then(function (response) { expect(response.body).to.eql({ name: 'Betty Boop', address: 'one & two' }) expect(response.headers['content-type']).to.equal('application/x-www-form-urlencoded; charset=utf-8') }) }) describe('multipart forms', function () { var filename = pathUtils.join(__dirname, 'afile.jpg') beforeEach(function () { return fs.writeFile(filename, 'an image') }) afterEach(function () { return fs.unlink(filename) }) it('can send multipart forms with `form-data`', function () { app.post('/form', function (req, res) { var form = new multiparty.Form() form.parse(req, function (err, fields, files) { if (err) { console.log(err) res.status(500).send({ message: err.message }) } var response = {} Object.keys(fields).forEach(function (field) { response[field] = fields[field][0] }) Promise.all(Object.keys(files).map(function (field) { var file = files[field][0] return streamToString(fs.createReadStream(file.path)).then(function (contents) { response[field] = { contents: contents, headers: file.headers } }) })).then(function () { res.send(response) }) }) }) var form = new FormData() form.append('name', 'Betty Boop') form.append('address', 'one & two') form.append('photo', fs.createReadStream(filename)) return httpism.post(baseurl + '/form', form).then(function (body) { expect(body).to.eql({ name: 'Betty Boop', address: 'one & two', photo: { contents: 'an image', headers: { 'content-disposition': 'form-data; name="photo"; filename="afile.jpg"', 'content-type': 'image/jpeg' } } }) }) }) }) }) describe('basic authentication', function () { beforeEach(function () { app.use(basicAuthConnect(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 (body) { expect(body).to.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 (body) { expect(body).to.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 (body) { expect(body).to.equal('this is secret') }) }) it("doesn't crash if username or password are undefined", function () { return httpism.get(baseurl + '/secret', { basicAuth: { }, exceptions: false, response: true }).then(function (response) { expect(response.statusCode).to.equal(401) }) }) 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) { expect(response.statusCode).to.equal(401) }) }) }) }) describe('streams', function () { var filename = pathUtils.join(__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 }, response: true }).then(function (response) { expect(response.headers['received-content-type']).to.equal(contentType) expect(response.body).to.equal('received: some content') }) }) } itCanUploadAStreamWithContentType('application/blah') itCanUploadAStreamWithContentType('application/json') itCanUploadAStreamWithContentType('text/plain') itCanUploadAStreamWithContentType('application/x-www-form-urlencoded') it('it guesses the Content-Type of the stream when created from a file', function () { var stream = fs.createReadStream(filename) return httpism.post(baseurl + '/file', stream, { response: true }).then(function (response) { expect(response.headers['received-content-type']).to.equal('text/plain') expect(response.body).to.equal('received: some content') }) }) it('can download a stream', function () { return httpism.get(baseurl + '/file', { response: true }).then(function (response) { expect(response.headers['content-type']).to.equal('application/blah') return streamToString(response.body).then(function (response) { expect(response).to.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', response: true }).then(function (response) { expect(response.headers['content-type']).to.contain(contentType) return streamToString(response.body).then(function (response) { expect(response).to.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, response: true }).then(function (response) { expect(response.headers['content-type']).to.equal('application/blah; charset=utf-8') expect(response.body).to.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 proxied var proxyAuth = false var proxyUrl = 'http://localhost:' + proxyPort + '/' var secureProxyUrl = 'http://bob:secret@localhost:' + proxyPort + '/' function proxyRequest (req, res) { urlProxied = req.url proxied = true proxy.web(req, res, { target: req.url }) } function checkProxyAuthentication (req, res, next) { var expectedAuthorisation = 'Basic ' + Buffer.from('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 proxied = false 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) { proxied = true 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(pathUtils.join(__dirname, 'server.key'), 'utf-8'), cert: fs.readFileSync(pathUtils.join(__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 (body) { expect(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 (body) { expect(body.protocol).to.equal('https') }) }) }) context('proxy environment variables', function () { beforeEach(function () { app.get('/', function (req, res) { res.send({ blah: 'blah' }) }) }) beforeEach(function () { delete process.env.NO_PROXY delete process.env.no_proxy delete process.env.HTTP_PROXY delete process.env.http_proxy delete process.env.HTTPS_PROXY delete process.env.https_proxy }) function assertProxied (url) { return httpism.get(url, { https: { rejectUnauthorized: false } }).then(function () { expect(proxied).to.equal(true) }) } function assertNotProxied (url) { return httpism.get(url, { https: { rejectUnauthorized: false } }).then(function () { expect(proxied).to.equal(false) }) } it('uses http_proxy for HTTP requests', function () { process.env.http_proxy = proxyUrl return assertProxied(baseurl) }) it('uses HTTP_PROXY for HTTP requests', function () { process.env.HTTP_PROXY = proxyUrl return assertProxied(baseurl) }) it('uses https_proxy for HTTPS requests', function () { process.env.https_proxy = proxyUrl return assertProxied(httpsBaseurl) }) it('uses HTTPS_PROXY for HTTPS requests', function () { process.env.HTTPS_PROXY = proxyUrl return assertProxied(httpsBaseurl) }) it('use skips hosts defined in no_proxy', function () { process.env.HTTP_PROXY = proxyUrl process.env.no_proxy = 'localhost' return assertNotProxied(httpsBaseurl) }) it('use skips hosts defined in NO_PROXY', function () { process.env.HTTP_PROXY = proxyUrl process.env.NO_PROXY = 'localhost' return assertNotProxied(baseurl) }) }) 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 (body) { expect(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 (body) { expect(body.protocol).to.equal('https') }) }) }) }) describe('raw', function () { it('can be used to create new