#--
# Copyright (c) 2007 Robert S. Thau, Smartleaf, Inc.
# 
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
require File.dirname(__FILE__) + '/../test_helper'

class UserTest < Test::Unit::TestCase

  use_all_fixtures

  def test_validations_and_permission_checks

    u = User.new
    assert !u.valid?

    test_validation u, :firm, :invalid => [nil], :valid => [firms(:mertz)]

    # Note that for name, we test the scoping by firm;
    # the Mertz firm already has a 'fred', so that's not valid,
    # but it's OK to create a 'ricky', even though Ricardo already
    # has one of those.

    test_validation u, :name,
      :invalid => [ '', 'a', 'ab', 'fred', 'x' * 101 ],
      :valid =>   [ 'ricky', 'rocky', 'bullwinkle', 'x' * 100 ]

    test_validation u, :full_name,
      :invalid => [ '', 'a', 'ab', 'x' * 101 ],
      :valid =>   [ 'rocky', 'bullwinkle', 'x' * 100 ]

    assert_valid u

    assert_requires( owner_firm_perm( :create, User, firms(:mertz) )) do
      assert u.save
    end

  end

  def test_rename_perm_check
    user = users(:fred)
    assert_requires( one_object_perm( :rename, user )) do
      user.name = 'frederick'
    end
    assert_requires( one_object_perm( :rename, user )) do
      user.full_name = 'Frederick Mertz'
    end
  end

  def test_as

    User.as( users(:fred) ) do
      assert_equal users(:fred), User.current
      assert_equal users(:fred), User.of_record
    end

    assert_nil User.current
    assert_nil User.of_record

    User.as( users(:fred), :acting_as => users(:ethel) ) do
      assert_equal users(:ethel), User.current
      assert_equal users(:fred),  User.of_record
    end

    assert_raises (ArgumentError) do 
      User.as( users(:fred), :cating_as => users(:ethel) )
    end

  end

  def test_password_fixture
    assert users(:fred).has_password?('ethel2')
  end

  def test_password_validation
    assert_invalid_password( 'bob',      [/short/, /digit/] )
    assert_invalid_password( 'aloysius', [/digit/] )
    assert_invalid_password( '01234567', [/letter/] )

    assert_invalid_password( 'bob23',    [/short/] )
    assert_valid_password( 'bob234' )
  end

  def assert_valid_password( pw )
    user = users(:ethel)
    assert_requires( one_object_perm( :set_password, user )) do
      user.password = pw
      user.password_confirmation = pw
    end
    assert_valid user
  end

  def assert_invalid_password( pw, expected_errors )
    user = users(:ethel)
    assert_requires( one_object_perm( :set_password, user )) do
      user.password = pw
      user.password_confirmation = pw
    end
    assert !user.valid?

    pw_errs = user.errors.on(:password)
    assert !pw_errs.nil?

    pw_errs = [pw_errs] unless pw_errs.is_a?( Array )

    assert_equal expected_errors.size, pw_errs.size

    expected_errors.each do |pattern|
      assert pw_errs.any?{ |msg| pattern === msg },
        "did not find expected error #{pattern} for password #{pw}"
    end
  end

  def test_password_confirmation_and_setting

    user = users(:ethel)
    assert_valid user

    user.password = ''
    assert_requires( one_object_perm( :set_password, user )) do
      user.password_confirmation = 'fred234'
    end
    assert !user.valid?
    assert_not_nil user.errors.on(:password)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = ''
    end
    assert !user.valid?
    assert_not_nil user.errors.on(:password)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password_confirmation = user.password
    end
    assert_valid user
    assert user.has_password?('fred234')

    user.save!

    # Big reload...

    user = User.find user.id

    assert_nil user.password
    assert_nil user.password_confirmation
    assert user.has_password?('fred234')

  end

  def test_current_pw_check

    User.as(users(:ricky)) do
      user = users(:ricky)
      assert user.has_password?('lucy3')
      assert user.has_current_password?('lucy3')

      user.password = 'luci11e'
      user.password_confirmation = 'luci11e'

      user.current_password_check = ''
      assert !user.save
      assert_equal 'is incorrect', user.errors.on( :current_password_check )

      user.current_password_check = 'lucy2'
      assert !user.save
      assert_equal 'is incorrect', user.errors.on( :current_password_check )

      assert user.has_current_password?('lucy3')
      user.current_password_check = 'lucy3'
      assert_valid user
      assert user.save

      user = User.find user.id
      assert user.has_password?( 'luci11e' )
    end

  end

  def test_password_salt

    user = users(:ethel)
    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end
    assert user.has_password?('fred234')

    salt0 = user.password_salt
    hash0 = user.password_hash

    user.save!
    user.reload

    # Hey, whaddya know... a test that can fail randomly,
    # if the code functions as designed... one time in
    # several billion runs.

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert user.has_password?('fred234')
    assert_not_equal salt0, user.password_salt
    assert_not_equal hash0, user.password_hash
    
  end

  def test_cant_reuse_password

    user = users(:ethel)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert user.has_password?('fred234')
    assert !user.has_current_password?('fred234')
    assert_valid user
    user.save!

    user = User.find user
    assert_nil user.password

    assert user.has_password?('fred234')
    assert user.has_current_password?('fred234')
    assert_nil user.password

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert !user.valid?
    assert_match /same/, user.errors.on(:password)

  end

  def test_set_pw_expiration

    user = users(:ethel)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert_valid user
    assert user.has_password?( 'fred234' )
    assert_nil user.password_expires_at

    firm = user.firm
    firm.password_lifetime_days = 7
    assert_nil user.password_expires_at # ?!?!?!?!?!

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred235'
      user.password_confirmation = 'fred235'
    end

    assert user.has_password?( 'fred235' )
    assert (7.days.from_now - user.password_expires_at) < 2 # seconds

  end

  def test_password_expiration

    user = users(:ethel)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert_valid user
    assert user.has_password?( 'fred234' )
    assert_nil user.last_login_at
    
    assert_nil user.current_password_remaining_days
    assert user.authenticate_by_password( 'fred234' )
    assert_nil user.authentication_status
    assert user.last_login_at > 2.seconds.ago

    assert_requires( one_object_perm( :set_password, user )) do
      user.password_expires_at = 3.days.from_now
    end

    assert_equal 3, user.current_password_remaining_days
    assert_valid user
    assert user.authenticate_by_password( 'fred234' )
    assert user.authentication_status.any? { |msg| /expires in 3 days/ === msg}

    assert_requires( one_object_perm( :set_password, user )) do
      user.password_expires_at = 1.days.ago
    end

    assert !user.authenticate_by_password( 'fred234' )
    assert user.authentication_status.any? { |msg| /expired/ === msg}
    
  end

  def test_bad_logins

    user = users(:ethel)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert_valid user
    assert user.has_password?( 'fred234' )
    assert_nil user.bad_login_attempts
    assert_nil user.bad_logins_since_lockout
    assert_nil user.no_login_until

    user.firm.max_bad_logins = 3
    user.firm.bad_login_dead_minutes = 5
    with_permission( wildcard_perm( :update, Firm )) do
      user.firm.save!
    end

    assert !user.authenticate_by_password( 'bart.simpson' )
    assert user.authentication_status.any? { |msg| /password/ === msg }
    assert_equal 1, user.bad_login_attempts
    assert_equal 1, user.bad_logins_since_lockout
    assert_nil      user.no_login_until

    assert !user.authenticate_by_password( 'bart.simpson' )
    assert user.authentication_status.any? { |msg| /password/ === msg }
    assert_equal 2, user.bad_login_attempts
    assert_equal 2, user.bad_logins_since_lockout
    assert_nil      user.no_login_until

    assert !user.authenticate_by_password( 'bart.simpson' )
    assert user.authentication_status.any? { |msg| /Too many/ === msg }
    assert_equal 3, user.bad_login_attempts
    assert_nil      user.bad_logins_since_lockout
    assert (5.minutes.from_now - user.no_login_until) < 2

    assert !user.authenticate_by_password( 'fred234' )
    assert user.authentication_status.any? { |msg| /Too many/ === msg }
    assert_equal 3, user.bad_login_attempts
    assert_nil      user.bad_logins_since_lockout
    assert (5.minutes.from_now - user.no_login_until) < 2

    user.no_login_until = 3.seconds.ago

    assert !user.authenticate_by_password( 'bart.simpson' )
    assert user.authentication_status.any? { |msg| /password/ === msg }
    assert_equal 4, user.bad_login_attempts
    assert_equal 1, user.bad_logins_since_lockout
    assert user.no_login_until < Time.now

    assert user.authenticate_by_password( 'fred234' )
    assert_nil user.bad_login_attempts
    assert_nil user.bad_logins_since_lockout
    assert_nil user.no_login_until
    assert user.authentication_status.any? { |msg| /4 failed login/ === msg }

  end

  def test_lockout_blocks_login

    user = users(:ethel)
    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end
    assert_valid user
    assert user.has_password?( 'fred234' )
    assert user.authenticate_by_password( 'fred234' )

    assert_requires( one_object_perm( :pw_administer, user )) do
      user.locked_out = true
    end
    assert !user.authenticate_by_password( 'fred234' )
    assert user.authentication_status.any? { |msg| /password/ === msg }
    
    assert_requires( one_object_perm( :pw_administer, user )) do
      user.locked_out = false
    end
    assert user.authenticate_by_password( 'fred234' )
    
  end

  def test_login_with_multiple_problems

    user = users(:ethel)

    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
      user.password_expires_at = 3.days.from_now
    end
    
    assert_valid user
    assert user.has_password?( 'fred234' )

    assert !user.authenticate_by_password( 'bart.simpson.rules' )
    assert_equal 1, user.authentication_status.size
    assert_match /password/, user.authentication_status.first

    assert user.authenticate_by_password( 'fred234' )
    assert_equal 2, user.authentication_status.size
    assert user.authentication_status.any?{ |msg| /failed login/ === msg }
    assert user.authentication_status.any?{ |msg| /expires/ === msg }

  end

  def test_reset_bad_logins

    user = users(:ethel)
    assert_requires( one_object_perm( :set_password, user )) do
      user.password = 'fred234'
      user.password_confirmation = 'fred234'
    end

    assert_valid user
    assert user.has_password?( 'fred234' )

    assert_nil user.bad_login_attempts
    assert_nil user.bad_logins_since_lockout
    assert_nil user.no_login_until

    user.firm.max_bad_logins = 1
    user.firm.bad_login_dead_minutes = 5
    with_permission( wildcard_perm( :update, Firm )) do
      user.firm.save!
    end

    assert !user.authenticate_by_password( 'bart.simpson' )
    assert !user.authenticate_by_password( 'bart.simpson' )
    
    assert_not_nil user.no_login_until
    assert_not_nil user.bad_login_attempts

    user_dummy = user.clone
    assert_not_nil user_dummy.no_login_until
    assert_not_nil user_dummy.bad_login_attempts
    
    assert_requires( wildcard_perm( :pw_administer, User )) do
      user_dummy.reset_bad_logins
    end
    assert_nil user_dummy.no_login_until
    assert_nil user_dummy.bad_login_attempts

    user_dummy = user.clone
    assert_requires( wildcard_perm( :pw_administer, User )) do
      user_dummy.reset_bad_logins_flag = false
    end
    assert_not_nil user_dummy.no_login_until
    assert_not_nil user_dummy.bad_login_attempts

    assert_requires( wildcard_perm( :pw_administer, User )) do
      user_dummy.reset_bad_logins_flag = true
    end
    assert_nil user_dummy.no_login_until
    assert_nil user_dummy.bad_login_attempts

  end

end

