require File.expand_path('../spec_helper', File.dirname(__FILE__))

describe 'Users' do

  def should_be_validated(user, password, message = "Password is invalid")
    assert(user.valid_ldap_authentication?(password), message)
  end

  def should_not_be_validated(user, password, message = "Password is not properly set")
     assert(!user.valid_ldap_authentication?(password), message)
  end

  describe "With default settings" do
    before do
      default_devise_settings!
      reset_ldap_server!
    end

    describe "look up and ldap user" do
      it "should return true for a user that does exist in LDAP" do
        assert_equal true, ::Devise::LDAP::Adapter.valid_login?('example.user@test.com')
      end

      it "should return false for a user that doesn't exist in LDAP" do
        assert_equal false, ::Devise::LDAP::Adapter.valid_login?('barneystinson')
      end
    end

    describe "create a basic user" do
      before do
        @user = Factory.create(:user)
      end

      it "should check for password validation" do
        assert_equal(@user.email, "example.user@test.com")
        should_be_validated @user, "secret"
        should_not_be_validated @user, "wrong_secret"
        should_not_be_validated @user, "Secret"
      end
    end

    describe "change a LDAP password" do
      before do
        @user = Factory.create(:user)
      end

      it "should change password" do
        should_be_validated @user, "secret"
        @user.password = "changed"
        @user.change_password!("secret")
        should_be_validated @user, "changed", "password was not changed properly on the LDAP sevrer"
      end

      it "should not allow to change password if setting is false" do
        should_be_validated @user, "secret"
        ::Devise.ldap_update_password = false
        @user.reset_password!("wrong_secret", "wrong_secret")
        should_not_be_validated @user, "wrong_secret"
        should_be_validated @user, "secret"
      end
    end

    describe "create new local user if user is in LDAP" do

      before do
        assert(User.all.blank?, "There shouldn't be any users in the database")
      end

      it "should not create user in the database" do
        @user = User.find_for_ldap_authentication(:email => "example.user@test.com", :password => "secret")
        assert(User.all.blank?)
        assert(@user.new_record?)
      end

      describe "creating users is enabled" do
        before do
          ::Devise.ldap_create_user = true
        end

        it "should create a user in the database" do
          @user = User.find_for_ldap_authentication(:email => "example.user@test.com", :password => "secret")
          assert_equal(User.all.size, 1)
          User.all.collect(&:email).should include("example.user@test.com")
          assert(@user.persisted?)
        end

        it "should not create a user in the database if the password is wrong_secret" do
          @user = User.find_for_ldap_authentication(:email => "example.user", :password => "wrong_secret")
          assert(User.all.blank?, "There's users in the database")
        end

        it "should not create a user if the user is not in LDAP" do
          @user = User.find_for_ldap_authentication(:email => "wrong_secret.user@test.com", :password => "wrong_secret")
          assert(User.all.blank?, "There's users in the database")
        end

        it "should create a user in the database if case insensitivity does not matter" do
          ::Devise.case_insensitive_keys = []
          @user = Factory.create(:user)

          expect do
            User.find_for_ldap_authentication(:email => "EXAMPLE.user@test.com", :password => "secret")
          end.to change { User.count }.by(1)
        end

        it "should not create a user in the database if case insensitivity matters" do
          ::Devise.case_insensitive_keys = [:email]
          @user = Factory.create(:user)

          expect do
            User.find_for_ldap_authentication(:email => "EXAMPLE.user@test.com", :password => "secret")
          end.to_not change { User.count }
        end

        it "should create a user with downcased email in the database if case insensitivity matters" do
          ::Devise.case_insensitive_keys = [:email]

          @user = User.find_for_ldap_authentication(:email => "EXAMPLE.user@test.com", :password => "secret")
          User.all.collect(&:email).should include("example.user@test.com")
        end
      end

    end

    describe "use groups for authorization" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
        ::Devise.authentication_keys = [:email]
        ::Devise.ldap_check_group_membership = true
      end

      it "should admin should be allowed in" do
        should_be_validated @admin, "admin_secret"
      end

      it "should admin should have the proper groups set" do
        @admin.ldap_groups.should include('cn=admins,ou=groups,dc=test,dc=com')
      end

      it "should user should not be allowed in" do
        should_not_be_validated @user, "secret"
      end
    end
    
    describe "check group membership" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
      end
      
      it "should return true for admin being in the admins group" do
        assert_equal true, @admin.in_ldap_group?('cn=admins,ou=groups,dc=test,dc=com')
      end
      
      it "should return false for admin being in the admins group using the 'foobar' group attribute" do
        assert_equal false, @admin.in_ldap_group?('cn=admins,ou=groups,dc=test,dc=com', 'foobar')
      end
      
      it "should return true for user being in the users group" do
        assert_equal true, @user.in_ldap_group?('cn=users,ou=groups,dc=test,dc=com')
      end   
      
      it "should return false for user being in the admins group" do
        assert_equal false, @user.in_ldap_group?('cn=admins,ou=groups,dc=test,dc=com')
      end

      it "should return false for a user being in a nonexistent group" do
        assert_equal false, @user.in_ldap_group?('cn=thisgroupdoesnotexist,ou=groups,dc=test,dc=com')
      end
    end

    describe "check group membership w/out admin bind" do
      before do
        @user = Factory.create(:user)
        ::Devise.ldap_check_group_membership_without_admin = true
      end

      after do
        ::Devise.ldap_check_group_membership_without_admin = false
      end

      it "should return true for user being in the users group" do
        assert_equal true, @user.in_ldap_group?('cn=users,ou=groups,dc=test,dc=com')
      end

      it "should return false for user being in the admins group" do
        assert_equal false, @user.in_ldap_group?('cn=admins,ou=groups,dc=test,dc=com')
      end

      it "should return false for a user being in a nonexistent group" do
        assert_equal false, @user.in_ldap_group?('cn=thisgroupdoesnotexist,ou=groups,dc=test,dc=com')
      end

      # TODO: add a test that confirms the user's own binding is used rather
      # than the admin binding by creating an LDAP user who can't do group
      # lookups perhaps?

      # TODO: add a test to demonstrate this function won't work on a user
      # after the initial login request if the password isn't available. This
      # might have to be more of a full stack test.
    end

    describe "use role attribute for authorization" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
        ::Devise.ldap_check_attributes = true
      end

      it "should admin should be allowed in" do
        should_be_validated @admin, "admin_secret"
      end

      it "should user should not be allowed in" do
        should_not_be_validated @user, "secret"
      end
    end

    describe "use admin setting to bind" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
        ::Devise.ldap_use_admin_to_bind = true
      end

      it "should description" do
        should_be_validated @admin, "admin_secret"
      end
    end

  end

  describe "use uid for login" do
    before do
      default_devise_settings!
      reset_ldap_server!
      ::Devise.ldap_config = "#{Rails.root}/config/#{"ssl_" if ENV["LDAP_SSL"]}ldap_with_uid.yml"
      ::Devise.authentication_keys = [:uid]
    end

    describe "description" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user, :uid => "example_user")
      end

      it "should be able to authenticate using uid" do
        should_be_validated @user, "secret"
        should_not_be_validated @admin, "admin_secret"
      end
    end

    describe "create user" do
      before do
        ::Devise.ldap_create_user = true
      end

      it "should create a user in the database" do
        @user = User.find_for_ldap_authentication(:uid => "example_user", :password => "secret")
        assert_equal(User.all.size, 1)
        User.all.collect(&:uid).should include("example_user")
      end

      it "should call ldap_before_save hooks" do
        User.class_eval do
          def ldap_before_save
            @foobar = 'foobar'
          end
        end
        user = User.find_for_ldap_authentication(:uid => "example_user", :password => "secret")
        assert_equal 'foobar', user.instance_variable_get(:"@foobar")
        User.class_eval do
          undef ldap_before_save
        end
      end

      it "should not call ldap_before_save hook if not defined" do
        should_be_validated Factory.create(:user, :uid => "example_user"), "secret"
      end
    end
  end

  describe "with failover hosts in the config file" do
    before do
      default_devise_settings!
      reset_ldap_server!
      ::Devise.ldap_config = "#{Rails.root}/config/ldap_with_failover.yml"
    end

    describe "authenticate" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
      end

      it "should be able to authenticate" do
        should_be_validated @user, "secret"
        should_be_validated @admin, "admin_secret"
      end

      describe "when all servers are down" do
        before :each do
          Net::LDAP.any_instance.stub(:search) do |a|
            raise Net::LDAP::LdapError.new("No such address or other socket error") 
          end
        end

        it "raises an exception" do
          expect { @user.valid_ldap_authentication?("secret") }.to raise_error(Net::LDAP::LdapError)
        end
      end

      describe "when the current server goes down" do
        before :each do
          Net::LDAP.any_instance.stub(:search) do |a|
            unless defined? @first_server
              @first_server = true
              raise Net::LDAP::LdapError.new("No such address or other socket error") 
            end
          end
        end

        it "fails over to the next server" do
          should_be_validated @user, "secret"
          should_be_validated @admin, "admin_secret"
        end
      end
    end
  end

  describe "using ERB in the config file" do
    before do
      default_devise_settings!
      reset_ldap_server!
      ::Devise.ldap_config = "#{Rails.root}/config/#{"ssl_" if ENV["LDAP_SSL"]}ldap_with_erb.yml"
    end

    describe "authenticate" do
      before do
        @admin = Factory.create(:admin)
        @user = Factory.create(:user)
      end

      it "should be able to authenticate" do
        should_be_validated @user, "secret"
        should_be_validated @admin, "admin_secret"
      end
    end
  end

  describe "using variants in the config file" do
    before do
      default_devise_settings!
      reset_ldap_server!
      ::Devise.ldap_config = Rails.root.join 'config', 'ldap_with_boolean_ssl.yml'
    end

    it "should not fail if config file has ssl: true" do
      Devise::LDAP::Connection.new
    end
  end

  describe "use username builder" do
    before do
      default_devise_settings!
      reset_ldap_server!
      ::Devise.ldap_auth_username_builder = Proc.new() do |attribute, login, ldap|
        "#{attribute}=#{login},ou=others,dc=test,dc=com"
      end
      @other = Factory.create(:other)
    end

    it "should be able to authenticate" do
      should_be_validated @other, "other_secret"
    end
  end

end
