From a6ac76c5c8d197ac13cae6c1aa3e08a55de76cf8 Mon Sep 17 00:00:00 2001 From: Jacob O'Mara Date: Thu, 14 Aug 2025 13:43:43 +0100 Subject: [PATCH] Bug 40824: Add tests for category-level password history Add comprehensive unit tests for the new password history functionality: - Add effective_password_history_count tests to Category.t - Create PatronPasswordHistory.t - Create PatronPasswordHistories.t - Add password history integration tests to Patron.t --- t/db_dependent/Koha/Patron.t | 195 ++++++++++++++++++++++++++ t/db_dependent/Koha/Patron/Category.t | 52 ++++++- 2 files changed, 246 insertions(+), 1 deletion(-) diff --git a/t/db_dependent/Koha/Patron.t b/t/db_dependent/Koha/Patron.t index 5af4dae347e..f38e02ff9b0 100755 --- a/t/db_dependent/Koha/Patron.t +++ b/t/db_dependent/Koha/Patron.t @@ -1807,6 +1807,201 @@ subtest 'force_password_reset_when_set_by_staff tests' => sub { $schema->storage->txn_rollback; }; +subtest 'password history integration tests' => sub { + plan tests => 7; + + $schema->storage->txn_begin; + + my $category = $builder->build_object( + { + class => 'Koha::Patron::Categories', + value => { + password_history_count => 3, + require_strong_password => 0, + } + } + ); + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { + categorycode => $category->categorycode, + password => Koha::AuthUtils::hash_password('initial_password') + } + } + ); + + # Test that password history is created when changing password + my $initial_history_count = + Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count; + + $patron->set_password( { password => 'new_password_1', skip_validation => 1 } ); + + my $new_history_count = + Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count; + + is( $new_history_count, $initial_history_count + 1, 'Password history entry created when password changed' ); + + # Test that UsedBefore exception is thrown for reused password + throws_ok { + $patron->set_password( { password => 'initial_password' } ); + } + 'Koha::Exceptions::Password::UsedBefore', 'UsedBefore exception thrown for reused password'; + + # Test that category-specific history count is respected + $patron->set_password( { password => 'new_password_2', skip_validation => 1 } ); + $patron->set_password( { password => 'new_password_3', skip_validation => 1 } ); + $patron->set_password( { password => 'new_password_4', skip_validation => 1 } ); + + my $final_history_count = + Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count; + + # With password_history_count=3, we should have 2 history entries (3-1 for current) + is( $final_history_count, 2, 'Password history cleanup respects category setting' ); + + # Test that changing category password_history_count affects validation + $category->password_history_count(1)->store; + + lives_ok { + $patron->set_password( { password => 'initial_password', skip_validation => 1 } ); + } + 'With history_count=1, old passwords from history are not checked'; + + throws_ok { + $patron->set_password( { password => 'initial_password' } ); + } + 'Koha::Exceptions::Password::UsedBefore', 'Current password still checked with history_count=1'; + + # Test with history_count=0 (no checking) + $category->password_history_count(0)->store; + + lives_ok { + $patron->set_password( { password => 'initial_password', skip_validation => 1 } ); + } + 'With history_count=0, no password history checking performed'; + + # Test fallback to system preference + $category->password_history_count(undef)->store; + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 ); + + throws_ok { + $patron->set_password( { password => 'initial_password' } ); + } + 'Koha::Exceptions::Password::UsedBefore', 'Falls back to system preference when category setting is null'; + + $schema->storage->txn_rollback; +}; + +subtest 'password history edge cases and overrides' => sub { + plan tests => 4; + + $schema->storage->txn_begin; + + # Test both category and system preference = 0 (should allow password reuse) + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 0 ); + + my $category_zero = $builder->build_object( + { + class => 'Koha::Patron::Categories', + value => { + password_history_count => 0, + require_strong_password => 0, + } + } + ); + + my $patron_zero = $builder->build_object( + { + class => 'Koha::Patrons', + value => { + categorycode => $category_zero->categorycode, + password => Koha::AuthUtils::hash_password('reusable_password') + } + } + ); + + # Should allow password reuse when both are 0 + lives_ok { + $patron_zero->set_password( { password => 'reusable_password' } ); + } + 'Password reuse allowed when both category and system preference are 0'; + + # Test category overrides system preference (category higher than system) + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 1 ); + + my $category_high = $builder->build_object( + { + class => 'Koha::Patron::Categories', + value => { + password_history_count => 3, + require_strong_password => 0, + } + } + ); + + my $patron_high = $builder->build_object( + { + class => 'Koha::Patrons', + value => { + categorycode => $category_high->categorycode, + password => Koha::AuthUtils::hash_password('test_password') + } + } + ); + + # Add some password history + $patron_high->set_password( { password => 'password1', skip_validation => 1 } ); + $patron_high->set_password( { password => 'password2', skip_validation => 1 } ); + + # Should prevent reuse of old password because category=3 overrides system=1 + throws_ok { + $patron_high->set_password( { password => 'test_password' } ); + } + 'Koha::Exceptions::Password::UsedBefore', 'Category setting (3) overrides lower system setting (1)'; + + # Test category overrides system preference (category lower than system) + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 5 ); + + my $category_low = $builder->build_object( + { + class => 'Koha::Patron::Categories', + value => { + password_history_count => 1, + require_strong_password => 0, + } + } + ); + + my $patron_low = $builder->build_object( + { + class => 'Koha::Patrons', + value => { + categorycode => $category_low->categorycode, + password => Koha::AuthUtils::hash_password('another_password') + } + } + ); + + # Add password history that would be caught by system=5 but not category=1 + $patron_low->set_password( { password => 'old_password1', skip_validation => 1 } ); + $patron_low->set_password( { password => 'old_password2', skip_validation => 1 } ); + + # Should allow reuse of original password because category=1 overrides system=5 + lives_ok { + $patron_low->set_password( { password => 'another_password', skip_validation => 1 } ); + } + 'Category setting (1) overrides higher system setting (5) - old password allowed'; + + # But current password should still be blocked + throws_ok { + $patron_low->set_password( { password => 'another_password' } ); + } + 'Koha::Exceptions::Password::UsedBefore', 'Current password still blocked with category=1'; + + $schema->storage->txn_rollback; +}; + subtest 'safe_to_delete() tests' => sub { plan tests => 17; diff --git a/t/db_dependent/Koha/Patron/Category.t b/t/db_dependent/Koha/Patron/Category.t index 9dfcc3563b8..651673e99ee 100755 --- a/t/db_dependent/Koha/Patron/Category.t +++ b/t/db_dependent/Koha/Patron/Category.t @@ -20,7 +20,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 9; +use Test::More tests => 10; use t::lib::TestBuilder; use t::lib::Mocks; @@ -310,6 +310,56 @@ subtest 'get_password_expiry_date() tests' => sub { }; +subtest 'effective_password_history_count' => sub { + plan tests => 5; + + $schema->storage->txn_begin; + + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 3 ); + + my $category = + $builder->build_object( { class => 'Koha::Patron::Categories', value => { password_history_count => undef } } ); + + is( + $category->effective_password_history_count, 3, + 'Category should use password history count from system preference' + ); + + $category->password_history_count(5)->store; + + is( + $category->effective_password_history_count, 5, + 'Category should use password history count from category setting' + ); + + # Test explicit 0 in category (should not fall back to system preference) + $category->password_history_count(0)->store; + + is( + $category->effective_password_history_count, 0, + 'Category setting of 0 should return 0, not fall back to system preference' + ); + + # Test system preference = 0, category = undef (should return 0) + t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 0 ); + $category->password_history_count(undef)->store; + + is( + $category->effective_password_history_count, 0, + 'Should return 0 when system preference is 0 and category is undef' + ); + + # Test both are 0 + $category->password_history_count(0)->store; + + is( + $category->effective_password_history_count, 0, + 'Should return 0 when both category and system preference are 0' + ); + + $schema->storage->txn_rollback; +}; + subtest 'can_make_suggestions' => sub { plan tests => 6; -- 2.39.5