require 'spec_helper'

describe AccessGranted::Policy do
  let(:klass)      { Class.new { include AccessGranted::Policy } }
  subject(:policy) { klass.new(nil) }

  describe "#configure" do
    before :each do
      @member = FakeUser.new(1, false, false, false)
      @mod    = FakeUser.new(2, true, false, false)
      @admin  = FakeUser.new(3, false, true, false)
      @banned = FakeUser.new(4, false, true, true)
    end

    it "passes user object to permission block" do
      post_owner = double(id: 123)
      other_user = double(id: 5)
      post = FakePost.new(1, post_owner.id)

      klass = Class.new do
        include AccessGranted::Policy

        def configure
          role :member do
            can :destroy, FakePost do |post, user|
              post.user_id == user.id
            end
          end
        end
      end

      expect(klass.new(post_owner).can?(:destroy, post)).to     eq(true)
      expect(klass.new(other_user).can?(:destroy, post)).to     eq(false)
    end

    it "selects permission based on role priority" do
      klass = Class.new do
        include AccessGranted::Policy

        def configure
          role :administrator, { is_admin: true } do
            can :destroy, String
          end

          role :moderator, { is_moderator: true } do
            can :update, String
          end

          role :member do
            can :read, String
          end
        end
      end
      expect(klass.new(@admin).can?(:destroy, String)).to     eq(true)
      expect(klass.new(@admin).can?(:read, String)).to        eq(true)

      expect(klass.new(@member).cannot?(:destroy, String)).to eq(true)
      expect(klass.new(@member).can?(:read, String)).to       eq(true)

      expect(klass.new(@mod).can?(:read, String)).to         eq(true)
      expect(klass.new(@mod).cannot?(:destroy, String)).to   eq(true)
    end

    context "when multiple roles define the same permission" do
      it "checks all roles until conditions are met" do
        user_post = FakePost.new(1, @member.id)
        other_post = FakePost.new(2, 66)

        klass = Class.new do
          include AccessGranted::Policy

          def configure
            role :administrator, { is_admin: true } do
              can :destroy, FakePost
            end

            role :member do
              can :destroy, FakePost do |post, user|
                post.user_id == user.id
              end
            end
          end
        end

       expect(klass.new(@admin).can?(:destroy, user_post)).to       eq(true)
       expect(klass.new(@admin).can?(:destroy, other_post)).to      eq(true)

       expect(klass.new(@member).can?(:destroy, user_post)).to      eq(true)
       expect(klass.new(@member).cannot?(:destroy, other_post)).to  eq(true)
      end
    end

    it "resolves permissions without subject" do
      klass = Class.new do
        include AccessGranted::Policy

        def configure
          role :member do
            can :vague_action
          end
        end
      end

      expect(klass.new(@member).can?(:vague_action)).to eq(true)
    end

    describe "#cannot" do
      it "forbids action when used in superior role" do
        klass = Class.new do
          include AccessGranted::Policy

          def configure
            role :banned, { is_banned: true } do
              cannot :create, String
            end

            role :member do
              can :create, String
            end
          end
        end
        expect(klass.new(@member).can?(:create, String)).to    eq(true)
        expect(klass.new(@banned).can?(:create, String)).to    eq(false)
      end
    end

    describe "#authorize!" do
      let(:klass) do
        Class.new do
          include AccessGranted::Policy

          def configure
            role(:member) { can :create, String }
          end
        end
      end

      it "raises AccessDenied if action is not allowed" do
        expect { klass.new(@member).authorize!(:create, Integer) }.to raise_error do |err|
          expect(err).to be_a(AccessGranted::AccessDenied)
          expect(err.action).to eq(:create)
          expect(err.subject).to eq(Integer)
        end
      end

      it "raises AccessDenied with supplied message if action is not allowed" do
        message = 'You are not allowed to create Integer'
        expect { klass.new(@member).authorize!(:create, Integer, message) }.to raise_error do |err|
          expect(err).to be_a(AccessGranted::AccessDenied)
          expect(err.action).to eq(:create)
          expect(err.subject).to eq(Integer)
          expect(err.message).to eq(message)
        end
      end

      it "returns the subject if allowed" do
        expect(klass.new(@member).authorize!(:create, String)).to equal String
      end
    end
  end

  describe "#role" do
    it "allows passing role class" do
      klass_role = Class.new AccessGranted::Role do
        def configure
          can :read, String
        end
      end
      subject.role(:member, klass_role)
      expect(policy.roles.first.class).to eq(klass_role)
    end

    it "returns roles in the order of priority" do
      policy.role(:admin)
      policy.role(:moderator)
      policy.role(:user)
      policy.role(:guest)

      expect(policy.roles.map(&:name)).to eq([:admin, :moderator, :user, :guest])
    end

    it "allows defining a default role" do
      policy.role(:member)
      expect(policy.roles.map(&:name)).to include(:member)
    end

    it "does not allow duplicate role names" do
      policy.role(:member)
      expect { policy.role(:member) }.to raise_error AccessGranted::DuplicateRole
    end

    it "allows nesting `can` calls inside a block" do
      role = policy.role(:member) do
        can :read, String
      end

      expect(role.find_permission(:read, String).granted).to eq(true)
    end
  end

  describe "#applicable_roles" do
    let(:user) { double("User", is_moderator: true, is_admin: true) }
    subject(:policy) { klass.new(user) }

    before do
      policy.role(:administrator, { is_admin:     true })
      policy.role(:moderator,     { is_moderator: true })
      policy.role(:member)
    end

    context "user matches all roles" do
      it "returns all matching roles in the order of priority" do
        expect(policy.applicable_roles.map(&:name)).to eq([:administrator, :moderator, :member])
      end
    end

    context "user is just an admin" do
      let(:user) { double("User", is_moderator: false, is_admin: true) }

      it 'returns array with admin and member roles' do
        expect(policy.applicable_roles.map(&:name)).to eq([:administrator, :member])
      end
    end
  end
end
