require 'spec_helper'

describe Grape::Middleware::Versioner::Header do
  let(:app) { ->(env) { [200, env, env] } }
  subject { Grape::Middleware::Versioner::Header.new(app, @options || {}) }

  before do
    @options = {
      version_options: {
        using: :header,
        vendor: 'vendor'
      }
    }
  end

  context 'api.type and api.subtype' do
    it 'sets type and subtype to first choice of content type if no preference given' do
      status, _, env = subject.call('HTTP_ACCEPT' => '*/*')
      expect(env['api.type']).to eql 'application'
      expect(env['api.subtype']).to eql 'vnd.vendor+xml'
      expect(status).to eq(200)
    end

    it 'sets preferred type' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/*')
      expect(env['api.type']).to eql 'application'
      expect(env['api.subtype']).to eql 'vnd.vendor+xml'
      expect(status).to eq(200)
    end

    it 'sets preferred type and subtype' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'text/plain')
      expect(env['api.type']).to eql 'text'
      expect(env['api.subtype']).to eql 'plain'
      expect(status).to eq(200)
    end
  end

  context 'api.format' do
    it 'is set' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
      expect(env['api.format']).to eql 'json'
      expect(status).to eq(200)
    end

    it 'is nil if not provided' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
      expect(env['api.format']).to eql nil
      expect(status).to eq(200)
    end

    ['v1', :v1].each do |version|
      context "when version is set to #{version}" do
        before do
          @options[:versions] = [version]
        end

        it 'is set' do
          status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
          expect(env['api.format']).to eql 'json'
          expect(status).to eq(200)
        end

        it 'is nil if not provided' do
          status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
          expect(env['api.format']).to eql nil
          expect(status).to eq(200)
        end
      end
    end
  end

  context 'api.vendor' do
    it 'is set' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor')
      expect(env['api.vendor']).to eql 'vendor'
      expect(status).to eq(200)
    end

    it 'is set if format provided' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor+json')
      expect(env['api.vendor']).to eql 'vendor'
      expect(status).to eq(200)
    end

    it 'fails with 406 Not Acceptable if vendor is invalid' do
      expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor+json').last }
        .to raise_exception do |exception|
          expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
          expect(exception.headers).to eql('X-Cascade' => 'pass')
          expect(exception.status).to eql 406
          expect(exception.message).to include 'API vendor not found'
        end
    end

    context 'when version is set' do
      before do
        @options[:versions] = ['v1']
      end

      it 'is set' do
        status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
        expect(env['api.vendor']).to eql 'vendor'
        expect(status).to eq(200)
      end

      it 'is set if format provided' do
        status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
        expect(env['api.vendor']).to eql 'vendor'
        expect(status).to eq(200)
      end

      it 'fails with 406 Not Acceptable if vendor is invalid' do
        expect { subject.call('HTTP_ACCEPT' => 'application/vnd.othervendor-v1+json').last }
          .to raise_exception do |exception|
            expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
            expect(exception.headers).to eql('X-Cascade' => 'pass')
            expect(exception.status).to eql 406
            expect(exception.message).to include('API vendor not found')
          end
      end
    end
  end

  context 'api.version' do
    before do
      @options[:versions] = ['v1']
    end

    it 'is set' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1')
      expect(env['api.version']).to eql 'v1'
      expect(status).to eq(200)
    end

    it 'is set if format provided' do
      status, _, env = subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json')
      expect(env['api.version']).to eql 'v1'
      expect(status).to eq(200)
    end

    it 'fails with 406 Not Acceptable if version is invalid' do
      expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').last }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
        expect(exception.headers).to eql('X-Cascade' => 'pass')
        expect(exception.status).to eql 406
        expect(exception.message).to include('API version not found')
      end
    end
  end

  it 'succeeds if :strict is not set' do
    expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
    expect(subject.call({}).first).to eq(200)
  end

  it 'succeeds if :strict is set to false' do
    @options[:version_options][:strict] = false
    expect(subject.call('HTTP_ACCEPT' => '').first).to eq(200)
    expect(subject.call({}).first).to eq(200)
  end

  context 'when :strict is set' do
    before do
      @options[:versions] = ['v1']
      @options[:version_options][:strict] = true
    end

    it 'fails with 406 Not Acceptable if header is not set' do
      expect { subject.call({}).last }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql('X-Cascade' => 'pass')
        expect(exception.status).to eql 406
        expect(exception.message).to include('Accept header must be set.')
      end
    end

    it 'fails with 406 Not Acceptable if header is empty' do
      expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql('X-Cascade' => 'pass')
        expect(exception.status).to eql 406
        expect(exception.message).to include('Accept header must be set.')
      end
    end

    it 'succeeds if proper header is set' do
      expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
    end
  end

  context 'when :strict and cascade: false' do
    before do
      @options[:versions] = ['v1']
      @options[:version_options][:strict] = true
      @options[:version_options][:cascade] = false
    end

    it 'fails with 406 Not Acceptable if header is not set' do
      expect { subject.call({}).last }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql({})
        expect(exception.status).to eql 406
        expect(exception.message).to include('Accept header must be set.')
      end
    end

    it 'fails with 406 Not Acceptable if header is application/xml' do
      expect { subject.call('HTTP_ACCEPT' => 'application/xml').last }
        .to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql({})
        expect(exception.status).to eql 406
        expect(exception.message).to include('API vendor or version not found.')
      end
    end

    it 'fails with 406 Not Acceptable if header is empty' do
      expect { subject.call('HTTP_ACCEPT' => '').last }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql({})
        expect(exception.status).to eql 406
        expect(exception.message).to include('Accept header must be set.')
      end
    end

    it 'fails with 406 Not Acceptable if header contains a single invalid accept' do
      expect { subject.call('HTTP_ACCEPT' => 'application/json;application/vnd.vendor-v1+json').first }
        .to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidAcceptHeader)
        expect(exception.headers).to eql({})
        expect(exception.status).to eql 406
        expect(exception.message).to include('API vendor or version not found.')
      end
    end

    it 'succeeds if proper header is set' do
      expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
    end
  end

  context 'when multiple versions are specified' do
    before do
      @options[:versions] = %w[v1 v2]
    end

    it 'succeeds with v1' do
      expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v1+json').first).to eq(200)
    end

    it 'succeeds with v2' do
      expect(subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v2+json').first).to eq(200)
    end

    it 'fails with another version' do
      expect { subject.call('HTTP_ACCEPT' => 'application/vnd.vendor-v3+json') }.to raise_exception do |exception|
        expect(exception).to be_a(Grape::Exceptions::InvalidVersionHeader)
        expect(exception.headers).to eql('X-Cascade' => 'pass')
        expect(exception.status).to eql 406
        expect(exception.message).to include('API version not found')
      end
    end
  end

  context 'when there are multiple versions with complex vendor specified with rescue_from :all' do
    subject do
      Class.new(Grape::API) do
        rescue_from :all
      end
    end

    let(:v1_app) do
      Class.new(Grape::API) do
        version 'v1', using: :header, vendor: 'test.a-cool_resource', cascade: false, strict: true
        content_type :v1_test, 'application/vnd.test.a-cool_resource-v1+json'
        formatter :v1_test, ->(object, _) { object }
        format :v1_test

        resources :users do
          get :hello do
            'one'
          end
        end
      end
    end

    let(:v2_app) do
      Class.new(Grape::API) do
        version 'v2', using: :header, vendor: 'test.a-cool_resource', strict: true
        content_type :v2_test, 'application/vnd.test.a-cool_resource-v2+json'
        formatter :v2_test, ->(object, _) { object }
        format :v2_test

        resources :users do
          get :hello do
            'two'
          end
        end
      end
    end

    def app
      subject.mount v2_app
      subject.mount v1_app
      subject
    end

    context 'with header versioned endpoints and a rescue_all block defined' do
      it 'responds correctly to a v1 request' do
        versioned_get '/users/hello', 'v1', using: :header, vendor: 'test.a-cool_resource'
        expect(last_response.body).to eq('one')
        expect(last_response.body).not_to include('API vendor or version not found')
      end

      it 'responds correctly to a v2 request' do
        versioned_get '/users/hello', 'v2', using: :header, vendor: 'test.a-cool_resource'
        expect(last_response.body).to eq('two')
        expect(last_response.body).not_to include('API vendor or version not found')
      end
    end
  end
end
