# frozen_string_literal: true

require 'spec_helper'

describe 'Gitlab::Graphql::Authorization' do
  set(:user) { create(:user) }

  let(:permission_single) { :foo }
  let(:permission_collection) { [:foo, :bar] }
  let(:test_object) { double(name: 'My name') }
  let(:query_string) { '{ object() { name } }' }
  let(:result) { execute_query(query_type)['data'] }

  subject { result['object'] }

  shared_examples 'authorization with a single permission' do
    it 'returns the protected field when user has permission' do
      permit(permission_single)

      expect(subject).to eq('name' => test_object.name)
    end

    it 'returns nil when user is not authorized' do
      expect(subject).to be_nil
    end
  end

  shared_examples 'authorization with a collection of permissions' do
    it 'returns the protected field when user has all permissions' do
      permit(*permission_collection)

      expect(subject).to eq('name' => test_object.name)
    end

    it 'returns nil when user only has one of the permissions' do
      permit(permission_collection.first)

      expect(subject).to be_nil
    end

    it 'returns nil when user only has none of the permissions' do
      expect(subject).to be_nil
    end
  end

  before do
    # By default, disallow all permissions.
    allow(Ability).to receive(:allowed?).and_return(false)
  end

  describe 'Field authorizations' do
    let(:type) { type_factory }

    describe 'with a single permission' do
      let(:query_type) do
        query_factory do |query|
          query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_single
        end
      end

      include_examples 'authorization with a single permission'
    end

    describe 'with a collection of permissions' do
      let(:query_type) do
        permissions = permission_collection
        query_factory do |qt|
          qt.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
            authorize permissions
          end
        end
      end

      include_examples 'authorization with a collection of permissions'
    end
  end

  describe 'Field authorizations when field is a built in type' do
    let(:query_type) do
      query_factory do |query|
        query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
      end
    end

    describe 'with a single permission' do
      let(:type) do
        type_factory do |type|
          type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
        end
      end

      it 'returns the protected field when user has permission' do
        permit(permission_single)

        expect(subject).to eq('name' => test_object.name)
      end

      it 'returns nil when user is not authorized' do
        expect(subject).to eq('name' => nil)
      end
    end

    describe 'with a collection of permissions' do
      let(:type) do
        permissions = permission_collection
        type_factory do |type|
          type.field :name, GraphQL::STRING_TYPE, null: true do
            authorize permissions
          end
        end
      end

      it 'returns the protected field when user has all permissions' do
        permit(*permission_collection)

        expect(subject).to eq('name' => test_object.name)
      end

      it 'returns nil when user only has one of the permissions' do
        permit(permission_collection.first)

        expect(subject).to eq('name' => nil)
      end

      it 'returns nil when user only has none of the permissions' do
        expect(subject).to eq('name' => nil)
      end
    end
  end

  describe 'Type authorizations' do
    let(:query_type) do
      query_factory do |query|
        query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
      end
    end

    describe 'with a single permission' do
      let(:type) do
        type_factory do |type|
          type.authorize permission_single
        end
      end

      include_examples 'authorization with a single permission'
    end

    describe 'with a collection of permissions' do
      let(:type) do
        type_factory do |type|
          type.authorize permission_collection
        end
      end

      include_examples 'authorization with a collection of permissions'
    end
  end

  describe 'type and field authorizations together' do
    let(:permission_1) { permission_collection.first }
    let(:permission_2) { permission_collection.last }

    let(:type) do
      type_factory do |type|
        type.authorize permission_1
      end
    end

    let(:query_type) do
      query_factory do |query|
        query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
      end
    end

    include_examples 'authorization with a collection of permissions'
  end

  describe 'type authorizations when applied to a relay connection' do
    let(:query_string) { '{ object() { edges { node { name } } } }' }

    let(:type) do
      type_factory do |type|
        type.authorize permission_single
      end
    end

    let(:query_type) do
      query_factory do |query|
        query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object] }
      end
    end

    subject { result.dig('object', 'edges') }

    it 'returns the protected field when user has permission' do
      permit(permission_single)

      expect(subject).not_to be_empty
      expect(subject.first['node']).to eq('name' => test_object.name)
    end

    it 'returns nil when user is not authorized' do
      expect(subject).to be_empty
    end
  end

  describe 'type authorizations when applied to a basic connection' do
    let(:type) do
      type_factory do |type|
        type.authorize permission_single
      end
    end

    let(:query_type) do
      query_factory do |query|
        query.field :object, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
      end
    end

    subject { result['object'].first }

    include_examples 'authorization with a single permission'
  end

  describe 'when connections do not follow the correct specification' do
    let(:query_string) { '{ object() { edges { node { name }} } }' }

    let(:type) do
      bad_node = type_factory do |type|
        type.graphql_name 'BadNode'
        type.field :bad_node, GraphQL::STRING_TYPE, null: true
      end

      type_factory do |type|
        type.field :edges, [bad_node], null: true
      end
    end

    let(:query_type) do
      query_factory do |query|
        query.field :object, type, null: true
      end
    end

    it 'throws an error' do
      expect { result }.to raise_error(Gitlab::Graphql::Errors::ConnectionDefinitionError)
    end
  end

  private

  def permit(*permissions)
    permissions.each do |permission|
      allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
    end
  end

  def type_factory
    Class.new(Types::BaseObject) do
      graphql_name 'TestType'

      field :name, GraphQL::STRING_TYPE, null: true

      yield(self) if block_given?
    end
  end

  def query_factory
    Class.new(Types::BaseObject) do
      graphql_name 'TestQuery'

      yield(self) if block_given?
    end
  end

  def execute_query(query_type)
    schema = Class.new(GraphQL::Schema) do
      use Gitlab::Graphql::Authorize
      query(query_type)
    end

    schema.execute(
      query_string,
      context: { current_user: user },
      variables: {}
    )
  end
end
