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
