require 'spec_helper'
require 'cors'

module CloudFoundry
  module Middleware
    RSpec.describe Cors do
      let(:allowed_domains) { ['http://*.inblue.net', 'http://talkoncorners.com', 'http://borrowedheaven.org'] }
      let(:middleware) { Cors.new(app, allowed_domains) }
      let(:app) { double(:app, call: [123, {}, 'a body']) }

      context 'when the Origin header is not present' do
        it 'does not return any Access-Control headers (the request is not a CORS request)' do
          _, headers, = middleware.call({})
          expect(headers['Access-Control']).to be_nil
        end

        it 'delegates to the initial request' do
          status, _, body = middleware.call({})
          expect(status).to eq(123)
          expect(body).to eq('a body')
        end
      end

      context 'when the Origin header is present' do
        describe 'preflight request' do
          context 'and the origin is not in the whitelist' do
            let(:origin) { 'http://corblimey.com' }

            it 'does not return any Access-Control headers' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
                'REQUEST_METHOD' => 'OPTIONS'
              }
              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control']).to be_nil
            end

            it 'delegates to the initial request' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
                'REQUEST_METHOD' => 'OPTIONS'
              }
              status, _, body = middleware.call(request_headers)

              expect(status).to eq(123)
              expect(body).to eq('a body')
            end
          end

          context 'and the origin is a subset of a domain in the whitelist, but does not match' do
            let(:origin) { 'http://talkoncorners.com.extra' }

            it 'does not return any Access-Control headers' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
                'REQUEST_METHOD' => 'OPTIONS'
              }
              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control']).to be_nil
            end

            it 'delegates to the initial request' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
                'REQUEST_METHOD' => 'OPTIONS'
              }
              status, _, body = middleware.call(request_headers)

              expect(status).to eq(123)
              expect(body).to eq('a body')
            end
          end

          context 'and the origin matches a domain in the whitelist' do
            let(:origin) { 'http://wildcarded.inblue.net' }

            context 'but no Access-Control-Request-Method header is present' do
              it 'does not return any Access-Control headers' do
                request_headers = {
                  'HTTP_ORIGIN' => origin,
                  'REQUEST_METHOD' => 'OPTIONS'
                }
                _, headers, = middleware.call(request_headers)

                expect(headers['Access-Control']).to be_nil
              end

              it 'delegates to the initial request' do
                request_headers = {
                  'HTTP_ORIGIN' => origin,
                  'REQUEST_METHOD' => 'OPTIONS'
                }
                status, _, body = middleware.call(request_headers)

                expect(status).to eq(123)
                expect(body).to eq('a body')
              end
            end

            context 'and the Access-Control-Request-Method header is present' do
              let(:request_headers) do
                {
                  'HTTP_ORIGIN' => origin,
                  'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET',
                  'REQUEST_METHOD' => 'OPTIONS'
                }
              end

              it 'returns a 200 code and does not process the original request' do
                status, _, body = middleware.call(request_headers)

                expect(status).to eq(200)
                expect(body).to eq('')
              end

              it 'sets the Content-Type: text/plain header' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Content-Type']).to eq('text/plain')
              end

              it 'returns a Vary: Origin header to ensure response is not cached for different origins' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Vary']).to eq('Origin')
              end

              it 'returns an Access-Control-Allow-Origin header containing the requested origin domain' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Allow-Origin']).to eq('http://wildcarded.inblue.net')
              end

              it 'allows credentials to be supplied' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Allow-Credentials']).to eq('true')
              end

              it 'returns the valid request methods in the Access-Control-Allow-Methods header' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Allow-Methods'].split(',')).to contain_exactly(
                  'PUT', 'POST', 'DELETE', 'GET'
                )
              end

              it 'returns a max-age header with a large value (since these headers rarely change' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Max-Age'].to_i).to be > 600
              end

              it 'allows custom headers to be returned' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Expose-Headers'].split(',')).
                  to contain_exactly('x-cf-warnings', 'x-app-staging-log', 'range', 'location', ::VCAP::Request::HEADER_NAME.downcase)
              end

              it 'allows needed request headers to be included' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Access-Control-Allow-Headers'].split(',')).to contain_exactly(
                  'origin',
                  'content-type',
                  'authorization'
                )
              end

              it 'returns Vary: Origin header' do
                _, headers, = middleware.call(request_headers)
                expect(headers['Vary'].split(',')).to contain_exactly('Origin')
              end

              context 'when the request asks to allow additional request headers' do
                let(:extra_headers) { { 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' => 'foo, bar, baz, Authorization' } }

                it 'allows that by adding them to the Allow-Headers list' do
                  _, headers, = middleware.call(request_headers.merge(extra_headers))
                  expect(headers['Access-Control-Allow-Headers'].split(',')).to contain_exactly(
                    'origin',
                    'content-type',
                    'authorization',
                    'foo', 'bar', 'baz'
                  )
                end
              end
            end
          end
        end

        describe 'a simple request or actual request' do
          context 'and the origin is not in the whitelist' do
            let(:origin) { 'http://corblimey.com' }

            it 'does not return any Access-Control headers' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control']).to be_nil
            end

            it 'delegates to the initial request' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              status, _, body = middleware.call(request_headers)

              expect(status).to eq(123)
              expect(body).to eq('a body')
            end
          end

          context 'and the origin is a subset of a domain in the whitelist, but does not match' do
            let(:origin) { 'http://talkoncorners.com.extra' }

            it 'does not return any Access-Control headers' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control']).to be_nil
            end

            it 'delegates to the initial request' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              status, _, body = middleware.call(request_headers)

              expect(status).to eq(123)
              expect(body).to eq('a body')
            end
          end

          context 'and the origin matches an entry in the whitelist' do
            let(:origin) { 'http://foo.inblue.net' }

            it 'delegates to the initial request' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              status, _, body = middleware.call(request_headers)

              expect(status).to eq(123)
              expect(body).to eq('a body')
            end

            it 'returns an Access-Control-Allow-Origin header containing the requested origin domain' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control-Allow-Origin']).to eq('http://foo.inblue.net')
            end

            it 'allows credentials to be supplied' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control-Allow-Credentials']).to eq('true')
            end

            it 'allows custom headers to be returned' do
              request_headers = {
                'HTTP_ORIGIN' => origin,
                'REQUEST_METHOD' => 'GET'
              }

              _, headers, = middleware.call(request_headers)

              expect(headers['Access-Control-Expose-Headers'].split(',')).
                to contain_exactly('x-cf-warnings', 'x-app-staging-log', 'range', 'location', ::VCAP::Request::HEADER_NAME.downcase)
            end

            describe 'Vary header' do
              it 'includes Origin' do
                request_headers = {
                  'HTTP_ORIGIN' => origin,
                  'REQUEST_METHOD' => 'GET'
                }

                _, headers, = middleware.call(request_headers)
                expect(headers['Vary'].split(',')).to include('Origin')
              end

              context 'when there are other values included' do
                before do
                  allow(app).to receive(:call).and_return([123, { 'Vary' => 'Pre-existing' }, 'a body'])
                end

                it 'maintains them' do
                  request_headers = {
                    'HTTP_ORIGIN' => origin,
                    'REQUEST_METHOD' => 'GET'
                  }

                  _, headers, = middleware.call(request_headers)
                  expect(headers['Vary'].split(',')).to contain_exactly('Origin', 'Pre-existing')
                end
              end
            end
          end
        end
      end
    end
  end
end
