openhim-core
Version:
The OpenHIM core application that provides logging and routing of http requests
843 lines (746 loc) • 28.4 kB
text/coffeescript
Error.stackTraceLimit = Infinity
fs = require "fs"
should = require "should"
sinon = require "sinon"
http = require "http"
router = require "../../lib/middleware/router"
testUtils = require "../testUtils"
Keystore = require("../../lib/model/keystore").Keystore
Certificate = require("../../lib/model/keystore").Certificate
Channel = require("../../lib/model/channels").Channel
describe "HTTP Router", ->
requestTimestamp = (new Date()).toString()
before (done) ->
testUtils.setupTestKeystore null, null, [], ->
done()
after (done) ->
testUtils.cleanupTestKeystore ->
done()
describe ".route", ->
it "should route an incomming request to the endpoints specific by the channel config", (done) ->
testUtils.createMockServer 201, "Mock response body\n", 9876, ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9876
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
ctx.response.status.should.be.exactly 201
ctx.response.body.toString().should.be.eql "Mock response body\n"
ctx.response.header.should.be.ok
done()
it 'should route binary data', (done) ->
testUtils.createStaticServer 'test/resources', 9337 , (server) ->
# Setup a channel for the mock endpoint
channel =
name: "Static Server Endpoint"
urlPattern: "/openhim-logo-green.png"
routes: [
host: "localhost"
port: 9337
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/openhim-logo-green.png"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
ctx.response.type.should.equal 'image/png'
ctx.response.body.toString().should.equal (fs.readFileSync 'test/resources/openhim-logo-green.png').toString()
server.close ->
done()
setupContextForMulticast = () ->
# Setup channels for the mock endpoints
channel =
name: "Multicast 1"
urlPattern: "test/multicast.+"
routes: [
name: "non_primary_1"
host: "localhost"
port: 7777
,
name: "primary"
host: "localhost"
port: 8888
primary: true
,
name: "non_primary_2"
host: "localhost"
port: 9999
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/test/multicasting"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
return ctx
it "should route an incomming https request to the endpoints specific by the channel config", (done) ->
testUtils.createMockHTTPSServerWithMutualAuth 201, "Mock response body\n", 9877, (server) ->
keystore = Keystore.findOne {}, (err, keystore) ->
cert = new Certificate
data: fs.readFileSync 'test/resources/server-tls/cert.pem'
keystore.ca.push cert
keystore.save ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
secured: true
host: 'localhost'
port: 9877
primary: true
cert: cert._id
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
router.route ctx, (err) ->
if err
return server.close ->
done err
ctx.response.status.should.be.exactly 201
ctx.response.body.toString().should.be.eql "Secured Mock response body\n"
ctx.response.header.should.be.ok
server.close done
it "should be denied access if the server doesn't know the client cert when using mutual TLS authentication", (done) ->
testUtils.createMockHTTPSServerWithMutualAuth 201, "Mock response body\n", 9877, false, (server) ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint mutual tls"
urlPattern: ".+"
allow: ['admin', 'aGroup', 'test']
authType: "public"
routes: [
{
name: "test mock"
secured: true
host: "localhost"
port: 9877
primary: true
}
]
txViewAcl: "aGroup"
(new Channel channel).save (err, ch1) ->
ctx = new Object()
ctx.authorisedChannel = ch1
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/test"
ctx.authorisedChannel._id = ch1._id
ctx.request.method = "GET"
router.route ctx, (err) ->
if err
logger.error err
return server.close ->
done err
ctx.response.status.should.be.exactly 500
ctx.response.body.toString().should.be.eql "An internal server error occurred"
if server
server.close done
else
done
it "should be able to multicast to multiple endpoints but return only the response from the primary route", (done) ->
testUtils.createMockServer 200, "Mock response body 1\n", 7777, ->
testUtils.createMockServer 201, "Mock response body 2\n", 8888, ->
testUtils.createMockServer 400, "Mock response body 3\n", 9999, ->
ctx = setupContextForMulticast()
router.route ctx, (err) ->
if err
return done err
ctx.response.status.should.be.exactly 201
ctx.response.body.toString().should.be.eql "Mock response body 2\n"
ctx.response.header.should.be.ok
done()
it "should be able to multicast to multiple endpoints and set the responses for non-primary routes in ctx.routes", (done) ->
testUtils.createMockServer 200, "Mock response body 1\n", 7750, ->
testUtils.createMockServer 201, "Mock response body 2\n", 7751, ->
testUtils.createMockServer 400, "Mock response body 3\n", 7752, ->
ctx = setupContextForMulticast()
router.route ctx, (err) ->
if err
return done err
setTimeout (->
ctx.routes.length.should.be.exactly 2
ctx.routes[0].response.status.should.be.exactly 200
ctx.routes[0].response.body.toString().should.be.eql "Mock response body 1\n"
ctx.routes[0].response.headers.should.be.ok
ctx.routes[0].request.path.should.be.exactly "/test/multicasting"
ctx.routes[0].request.timestamp.should.be.exactly requestTimestamp
ctx.routes[1].response.status.should.be.exactly 400
ctx.routes[1].response.body.toString().should.be.eql "Mock response body 3\n"
ctx.routes[1].response.headers.should.be.ok
ctx.routes[1].request.path.should.be.exactly "/test/multicasting"
ctx.routes[1].request.timestamp.should.be.exactly requestTimestamp
done()
), 100 * global.testTimeoutFactor
it "should pass an error to next if there are multiple primary routes", (done) ->
testUtils.createMockServer 200, "Mock response body 1\n", 4444, (mock1) ->
testUtils.createMockServer 201, "Mock response body 2\n", 5555, (mock2) ->
testUtils.createMockServer 400, "Mock response body 3\n", 6666, (mock3) ->
# Setup channels for the mock endpoints
channel =
name: "Multi-primary"
urlPattern: "test/multi-primary"
routes: [
host: "localhost"
port: 4444
,
host: "localhost"
port: 5555
primary: true
,
host: "localhost"
port: 6666
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.request.url = "/test/multi-primary"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
err.message.should.be.exactly "Cannot route transaction: Channel contains multiple primary routes and only one primary is allowed"
mock1.close -> mock2.close -> mock3.close done
it "should forward PUT and POST requests correctly", (done) ->
# Create mock endpoint to forward requests to
mockServer = testUtils.createMockServerForPost(200, 400, "TestBody")
mockServer.listen 3333, ->
# Setup a channel for the mock endpoint
channel =
name: "POST channel"
urlPattern: ".+"
routes: [
host: "localhost"
port: 3333
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "POST"
ctx.requestTimestamp = requestTimestamp
ctx.body = "TestBody"
router.route ctx, (err) ->
if err
return done err
ctx.response.status.should.be.exactly 200
ctx.response.header.should.be.ok
mockServer.close done
it "should send request params if these where received from the incoming request", (done) ->
mockServer = testUtils.createMockServer 201, "Mock response body\n", 9873, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9873
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = "/test"
ctx.request.url = "/test?parma1=val1&parma2=val2"
ctx.request.method = "GET"
ctx.request.querystring = "parma1=val1&parma2=val2"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
req.url.should.eql("/test?parma1=val1&parma2=val2")
mockServer.close done
it "should set mediator response object on ctx", (done) ->
mediatorResponse =
status: 'Successful'
response:
status: 201
headers: {}
body: 'Mock response body\n'
orchestrations:
name: 'Mock mediator orchestration'
request:
path: '/some/path'
method: 'GET'
timestamp: (new Date()).toString()
response:
status: 200
body: 'Orchestrated response'
timestamp: (new Date()).toString()
properties:
prop1: 'val1'
prop2: 'val2'
testUtils.createMockMediatorServer 201, mediatorResponse, 9878, ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9878
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
try
ctx.response.status.should.be.exactly 201
ctx.mediatorResponse.should.exist
ctx.mediatorResponse.should.eql mediatorResponse
done()
catch err
done err
it "should set mediator response data as response to client", (done) ->
mediatorResponse =
status: 'Failed'
response:
status: 400
headers: { 'content-type': 'text/xml', 'another-header': 'xyz' }
body: 'Mock response body from mediator\n'
orchestrations:
name: 'Mock mediator orchestration'
request:
path: '/some/path'
method: 'GET'
timestamp: (new Date()).toString()
response:
status: 200
body: 'Orchestrated response'
timestamp: (new Date()).toString()
properties:
prop1: 'val1'
prop2: 'val2'
testUtils.createMockMediatorServer 201, mediatorResponse, 9879, ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9879
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = sinon.spy()
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
try
ctx.response.status.should.be.exactly 400
ctx.response.body.should.be.exactly 'Mock response body from mediator\n'
ctx.response.type.should.be.exactly 'text/xml'
(ctx.response.set.calledWith 'another-header', 'xyz').should.be.true
done()
catch err
done err
it "should set mediator response data for non-primary routes", (done) ->
router.nonPrimaryRoutes = []
mediatorResponse =
status: 'Failed'
response:
status: 400
headers: {}
body: 'Mock response body from mediator\n'
orchestrations:
name: 'Mock mediator orchestration'
request:
path: '/some/path'
method: 'GET'
timestamp: (new Date()).toString()
response:
status: 200
body: 'Orchestrated response'
timestamp: (new Date()).toString()
properties:
prop1: 'val1'
prop2: 'val2'
testUtils.createMockMediatorServer 201, mediatorResponse, 9888, ->
testUtils.createMockMediatorServer 201, mediatorResponse, 9889, ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
name: 'non prim'
host: "localhost"
port: 9889
,
name: 'primary'
host: "localhost"
port: 9888
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
setTimeout (->
try
ctx.routes[0].response.body.toString().should.be.eql "Mock response body from mediator\n"
ctx.routes[0].orchestrations.should.be.eql mediatorResponse.orchestrations
ctx.routes[0].properties.should.be.eql mediatorResponse.properties
done()
catch err
done err
), 50 * global.testTimeoutFactor
it "should set mediator response location header if present and status is not 3xx", (done) ->
mediatorResponse =
status: 'Successful'
response:
status: 201
headers:
location: 'Patient/1/_history/1'
body: 'Mock response body\n'
orchestrations:
name: 'Mock mediator orchestration'
request:
path: '/some/path'
method: 'GET'
timestamp: (new Date()).toString()
response:
status: 200
body: 'Orchestrated response'
timestamp: (new Date()).toString()
properties:
prop1: 'val1'
prop2: 'val2'
testUtils.createMockMediatorServer 201, mediatorResponse, 9899, ->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9899
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
headerSpy = {}
ctx.response.set = (k, v) -> headerSpy[k] = v
router.route ctx, (err) ->
if err
return done err
try
headerSpy.should.have.property 'location', mediatorResponse.response.headers.location
done()
catch err
done err
describe "Basic Auth", ->
it "should have valid authorization header if username and password is set in options", (done) ->
mockServer = testUtils.createMockServer 201, "Mock response body\n", 9875, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9875
primary: true
username: "username"
password: "password"
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
# Base64("username:password") = "dXNlcm5hbWU6cGFzc3dvcmQ=""
req.headers.authorization.should.be.exactly "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
mockServer.close done
it "should not have authorization header if username and password is absent from options", (done) ->
mockServer = testUtils.createMockServer 201, "Mock response body\n", 9874, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9874
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
(req.headers.authorization == undefined).should.be.true
mockServer.close done
it "should not propagate the authorization header present in the request headers", (done) ->
mockServer = testUtils.createMockServer 201, "Mock response body\n", 9872, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9872
primary: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.request.header = { authorization: "Basic bWU6bWU=" }
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
(req.headers.authorization == undefined).should.be.true
mockServer.close done
it "should propagate the authorization header present in the request headers if forwardAuthHeader is set to true", (done) ->
mockServer = testUtils.createMockServer 201, "Mock response body\n", 9872, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9872
primary: true
forwardAuthHeader: true
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.request.header = { authorization: "Basic bWU6bWU=" }
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
req.headers.authorization.should.be.exactly "Basic bWU6bWU="
mockServer.close done
it "should not propagate the authorization header present in the request headers and must set the correct header if enabled on route", (done) ->
testUtils.createMockServer 201, "Mock response body\n", 9871, (->
# Setup a channel for the mock endpoint
channel =
name: "Mock endpoint"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9871
primary: true
username: "username"
password: "password"
]
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.request.header = { authorization: "Basic bWU6bWU=" }
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
), (req, res) ->
# Base64("username:password") = "dXNlcm5hbWU6cGFzc3dvcmQ=""
req.headers.authorization.should.be.exactly "Basic dXNlcm5hbWU6cGFzc3dvcmQ="
done()
describe "Path Redirection", ->
describe ".transformPath", ->
it "must transform the path string correctly", (done) ->
test = (path, expr, res) -> router.transformPath(path, expr).should.be.exactly res
test("foo", "s/foo/bar", "bar")
test("foo", "s/foo/", "")
test("foo", "s/o/e/g", "fee")
test("foofoo", "s/foo//g", "")
test("foofoofoo", "s/foo/bar", "barfoofoo")
test("foofoofoo", "s/foo/bar/g", "barbarbar")
test("foo/bar", "s/foo/bar", "bar/bar")
test("foo/bar", "s/foo\\\/bar/", "")
test("foo/foo/bar/bar", "s/\\\/foo\\\/bar/", "foo/bar")
test("prefix/foo/bar", "s/prefix\\\//", "foo/bar")
done()
testPathRedirectionRouting = (mockServerPort, channel, expectedTargetPath, callback) ->
setup = () ->
ctx = new Object()
ctx.authorisedChannel = channel
ctx.request = new Object()
ctx.response = new Object()
ctx.response.set = ->
ctx.path = ctx.request.url = "/test"
ctx.request.method = "GET"
ctx.requestTimestamp = requestTimestamp
router.route ctx, (err) ->
if err
return done err
ctx.response.status.should.be.exactly 200
ctx.response.body.toString().should.be.eql "Mock response body\n"
ctx.response.header.should.be.ok
testUtils.createMockServer 200, "Mock response body\n", mockServerPort, setup, (req, res) ->
req.url.should.be.exactly expectedTargetPath
callback()
it "should redirect the request to a specific path", (done) ->
channel =
name: "Path test"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9886
path: "/target"
primary: true
]
testPathRedirectionRouting 9886, channel, "/target", done
it "should redirect the request to the transformed path", (done) ->
channel =
name: "Path test"
urlPattern: ".+"
routes: [
host: "localhost"
port: 9887
pathTransform: "s/test/target"
primary: true
]
testPathRedirectionRouting 9887, channel, "/target", done
describe 'setKoaResponse', ->
createCtx = ->
ctx = {}
ctx.response = {}
ctx.response.set = sinon.spy()
return ctx
createResponse = ->
return response =
status: 201
headers:
'content-type': 'text/xml'
'x-header': 'anotherValue'
timestamp: new Date()
body: 'Mock response body'
it 'should set the ctx.response object', ->
# given
ctx = createCtx()
response = createResponse()
# when
router.setKoaResponse ctx, response
# then
ctx.response.status.should.be.exactly response.status
ctx.response.body.should.be.exactly response.body
ctx.response.timestamp.should.be.exactly response.timestamp
it 'should copy response headers to the ctx.response object', ->
# given
ctx = createCtx()
response = createResponse()
# when
router.setKoaResponse ctx, response
# then
(ctx.response.set.calledWith 'x-header', 'anotherValue').should.be.true
it 'should redirect the context if needed', ->
# given
ctx = createCtx()
ctx.response.redirect = sinon.spy()
response =
status: 301
headers:
'content-type': 'text/xml'
'x-header': 'anotherValue'
'location': 'http://some.other.place.org'
timestamp: new Date()
body: 'Mock response body'
# when
router.setKoaResponse ctx, response
# then
(ctx.response.redirect.calledWith 'http://some.other.place.org').should.be.true
it 'should not redirect if a non-redirect status is recieved', ->
# given
ctx = createCtx()
ctx.response.redirect = sinon.spy()
response =
status: 201
headers:
'content-type': 'text/xml'
'x-header': 'anotherValue'
'location': 'http://some.other.place.org'
timestamp: new Date()
body: 'Mock response body'
# when
router.setKoaResponse ctx, response
# then
(ctx.response.redirect.calledWith 'http://some.other.place.org').should.be.false