View | Details | Raw Unified | Return to bug 40824
Collapse All | Expand All

(-)a/t/db_dependent/Koha/Patron.t (-1 / +196 lines)
Lines 19-25 Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::More tests => 42;
22
use Test::More tests => 44;
23
use Test::NoWarnings;
23
use Test::NoWarnings;
24
use Test::Exception;
24
use Test::Exception;
25
use Test::Warn;
25
use Test::Warn;
Lines 1807-1812 subtest 'force_password_reset_when_set_by_staff tests' => sub { Link Here
1807
    $schema->storage->txn_rollback;
1807
    $schema->storage->txn_rollback;
1808
};
1808
};
1809
1809
1810
subtest 'password history integration tests' => sub {
1811
    plan tests => 7;
1812
1813
    $schema->storage->txn_begin;
1814
1815
    my $category = $builder->build_object(
1816
        {
1817
            class => 'Koha::Patron::Categories',
1818
            value => {
1819
                password_history_count  => 3,
1820
                require_strong_password => 0,
1821
            }
1822
        }
1823
    );
1824
1825
    my $patron = $builder->build_object(
1826
        {
1827
            class => 'Koha::Patrons',
1828
            value => {
1829
                categorycode => $category->categorycode,
1830
                password     => Koha::AuthUtils::hash_password('initial_password')
1831
            }
1832
        }
1833
    );
1834
1835
    # Test that password history is created when changing password
1836
    my $initial_history_count =
1837
        Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
1838
1839
    $patron->set_password( { password => 'new_password_1', skip_validation => 1 } );
1840
1841
    my $new_history_count =
1842
        Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
1843
1844
    is( $new_history_count, $initial_history_count + 1, 'Password history entry created when password changed' );
1845
1846
    # Test that UsedBefore exception is thrown for reused password
1847
    throws_ok {
1848
        $patron->set_password( { password => 'initial_password' } );
1849
    }
1850
    'Koha::Exceptions::Password::UsedBefore', 'UsedBefore exception thrown for reused password';
1851
1852
    # Test that category-specific history count is respected
1853
    $patron->set_password( { password => 'new_password_2', skip_validation => 1 } );
1854
    $patron->set_password( { password => 'new_password_3', skip_validation => 1 } );
1855
    $patron->set_password( { password => 'new_password_4', skip_validation => 1 } );
1856
1857
    my $final_history_count =
1858
        Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
1859
1860
    # With password_history_count=3, we should have 2 history entries (3-1 for current)
1861
    is( $final_history_count, 2, 'Password history cleanup respects category setting' );
1862
1863
    # Test that changing category password_history_count affects validation
1864
    $category->password_history_count(1)->store;
1865
1866
    lives_ok {
1867
        $patron->set_password( { password => 'initial_password', skip_validation => 1 } );
1868
    }
1869
    'With history_count=1, old passwords from history are not checked';
1870
1871
    throws_ok {
1872
        $patron->set_password( { password => 'initial_password' } );
1873
    }
1874
    'Koha::Exceptions::Password::UsedBefore', 'Current password still checked with history_count=1';
1875
1876
    # Test with history_count=0 (no checking)
1877
    $category->password_history_count(0)->store;
1878
1879
    lives_ok {
1880
        $patron->set_password( { password => 'initial_password', skip_validation => 1 } );
1881
    }
1882
    'With history_count=0, no password history checking performed';
1883
1884
    # Test fallback to system preference
1885
    $category->password_history_count(undef)->store;
1886
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 );
1887
1888
    throws_ok {
1889
        $patron->set_password( { password => 'initial_password' } );
1890
    }
1891
    'Koha::Exceptions::Password::UsedBefore', 'Falls back to system preference when category setting is null';
1892
1893
    $schema->storage->txn_rollback;
1894
};
1895
1896
subtest 'password history edge cases and overrides' => sub {
1897
    plan tests => 4;
1898
1899
    $schema->storage->txn_begin;
1900
1901
    # Test both category and system preference = 0 (should allow password reuse)
1902
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 0 );
1903
1904
    my $category_zero = $builder->build_object(
1905
        {
1906
            class => 'Koha::Patron::Categories',
1907
            value => {
1908
                password_history_count  => 0,
1909
                require_strong_password => 0,
1910
            }
1911
        }
1912
    );
1913
1914
    my $patron_zero = $builder->build_object(
1915
        {
1916
            class => 'Koha::Patrons',
1917
            value => {
1918
                categorycode => $category_zero->categorycode,
1919
                password     => Koha::AuthUtils::hash_password('reusable_password')
1920
            }
1921
        }
1922
    );
1923
1924
    # Should allow password reuse when both are 0
1925
    lives_ok {
1926
        $patron_zero->set_password( { password => 'reusable_password' } );
1927
    }
1928
    'Password reuse allowed when both category and system preference are 0';
1929
1930
    # Test category overrides system preference (category higher than system)
1931
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 1 );
1932
1933
    my $category_high = $builder->build_object(
1934
        {
1935
            class => 'Koha::Patron::Categories',
1936
            value => {
1937
                password_history_count  => 3,
1938
                require_strong_password => 0,
1939
            }
1940
        }
1941
    );
1942
1943
    my $patron_high = $builder->build_object(
1944
        {
1945
            class => 'Koha::Patrons',
1946
            value => {
1947
                categorycode => $category_high->categorycode,
1948
                password     => Koha::AuthUtils::hash_password('test_password')
1949
            }
1950
        }
1951
    );
1952
1953
    # Add some password history
1954
    $patron_high->set_password( { password => 'password1', skip_validation => 1 } );
1955
    $patron_high->set_password( { password => 'password2', skip_validation => 1 } );
1956
1957
    # Should prevent reuse of old password because category=3 overrides system=1
1958
    throws_ok {
1959
        $patron_high->set_password( { password => 'test_password' } );
1960
    }
1961
    'Koha::Exceptions::Password::UsedBefore', 'Category setting (3) overrides lower system setting (1)';
1962
1963
    # Test category overrides system preference (category lower than system)
1964
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 5 );
1965
1966
    my $category_low = $builder->build_object(
1967
        {
1968
            class => 'Koha::Patron::Categories',
1969
            value => {
1970
                password_history_count  => 1,
1971
                require_strong_password => 0,
1972
            }
1973
        }
1974
    );
1975
1976
    my $patron_low = $builder->build_object(
1977
        {
1978
            class => 'Koha::Patrons',
1979
            value => {
1980
                categorycode => $category_low->categorycode,
1981
                password     => Koha::AuthUtils::hash_password('another_password')
1982
            }
1983
        }
1984
    );
1985
1986
    # Add password history that would be caught by system=5 but not category=1
1987
    $patron_low->set_password( { password => 'old_password1', skip_validation => 1 } );
1988
    $patron_low->set_password( { password => 'old_password2', skip_validation => 1 } );
1989
1990
    # Should allow reuse of original password because category=1 overrides system=5
1991
    lives_ok {
1992
        $patron_low->set_password( { password => 'another_password', skip_validation => 1 } );
1993
    }
1994
    'Category setting (1) overrides higher system setting (5) - old password allowed';
1995
1996
    # But current password should still be blocked
1997
    throws_ok {
1998
        $patron_low->set_password( { password => 'another_password' } );
1999
    }
2000
    'Koha::Exceptions::Password::UsedBefore', 'Current password still blocked with category=1';
2001
2002
    $schema->storage->txn_rollback;
2003
};
2004
1810
subtest 'safe_to_delete() tests' => sub {
2005
subtest 'safe_to_delete() tests' => sub {
1811
2006
1812
    plan tests => 17;
2007
    plan tests => 17;
(-)a/t/db_dependent/Koha/Patron/Category.t (-1 / +51 lines)
Lines 20-26 Link Here
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Test::NoWarnings;
22
use Test::NoWarnings;
23
use Test::More tests => 9;
23
use Test::More tests => 10;
24
24
25
use t::lib::TestBuilder;
25
use t::lib::TestBuilder;
26
use t::lib::Mocks;
26
use t::lib::Mocks;
Lines 310-315 subtest 'get_password_expiry_date() tests' => sub { Link Here
310
310
311
};
311
};
312
312
313
subtest 'effective_password_history_count' => sub {
314
    plan tests => 5;
315
316
    $schema->storage->txn_begin;
317
318
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 3 );
319
320
    my $category =
321
        $builder->build_object( { class => 'Koha::Patron::Categories', value => { password_history_count => undef } } );
322
323
    is(
324
        $category->effective_password_history_count, 3,
325
        'Category should use password history count from system preference'
326
    );
327
328
    $category->password_history_count(5)->store;
329
330
    is(
331
        $category->effective_password_history_count, 5,
332
        'Category should use password history count from category setting'
333
    );
334
335
    # Test explicit 0 in category (should not fall back to system preference)
336
    $category->password_history_count(0)->store;
337
338
    is(
339
        $category->effective_password_history_count, 0,
340
        'Category setting of 0 should return 0, not fall back to system preference'
341
    );
342
343
    # Test system preference = 0, category = undef (should return 0)
344
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 0 );
345
    $category->password_history_count(undef)->store;
346
347
    is(
348
        $category->effective_password_history_count, 0,
349
        'Should return 0 when system preference is 0 and category is undef'
350
    );
351
352
    # Test both are 0
353
    $category->password_history_count(0)->store;
354
355
    is(
356
        $category->effective_password_history_count, 0,
357
        'Should return 0 when both category and system preference are 0'
358
    );
359
360
    $schema->storage->txn_rollback;
361
};
362
313
subtest 'can_make_suggestions' => sub {
363
subtest 'can_make_suggestions' => sub {
314
364
315
    plan tests => 6;
365
    plan tests => 6;
(-)a/t/db_dependent/Koha/PatronPasswordHistories.t (+265 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# Copyright 2025 Koha Development team
4
#
5
# This file is part of Koha
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 4;
23
use Test::NoWarnings;
24
25
use t::lib::TestBuilder;
26
use t::lib::Mocks;
27
28
use Koha::Database;
29
use Koha::PatronPasswordHistory;
30
use Koha::PatronPasswordHistories;
31
use Koha::AuthUtils;
32
33
my $schema  = Koha::Database->new->schema;
34
my $builder = t::lib::TestBuilder->new;
35
36
subtest 'cleanup_old_password_history' => sub {
37
    plan tests => 6;
38
39
    $schema->storage->txn_begin;
40
41
    my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
42
43
    # Create 5 password history entries
44
    my @passwords = ();
45
    for my $i ( 1 .. 5 ) {
46
        my $password_history = Koha::PatronPasswordHistory->new(
47
            {
48
                borrowernumber => $patron->borrowernumber,
49
                password       => Koha::AuthUtils::hash_password("password_$i")
50
            }
51
        );
52
        $password_history->store;
53
        push @passwords, $password_history;
54
        sleep(1);    # Ensure different timestamps
55
    }
56
57
    my $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
58
    is( $count, 5, 'Initially have 5 password history entries' );
59
60
    # Keep 3 most recent
61
    my $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 3 );
62
    is( $deleted, 2, 'Deleted 2 old entries' );
63
64
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
65
    is( $count, 3, 'Now have 3 password history entries' );
66
67
    # Keep 1 most recent
68
    $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 1 );
69
    is( $deleted, 2, 'Deleted 2 more entries' );
70
71
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
72
    is( $count, 1, 'Now have 1 password history entry' );
73
74
    # Delete all (count = 0)
75
    $deleted = Koha::PatronPasswordHistories->cleanup_old_password_history( $patron->borrowernumber, 0 );
76
    is( $deleted, 1, 'Deleted remaining entry' );
77
78
    $schema->storage->txn_rollback;
79
};
80
81
subtest 'has_used_password with category settings' => sub {
82
    plan tests => 10;
83
84
    $schema->storage->txn_begin;
85
86
    my $category = $builder->build_object(
87
        {
88
            class => 'Koha::Patron::Categories',
89
            value => { password_history_count => 3 }
90
        }
91
    );
92
    my $patron = $builder->build_object(
93
        {
94
            class => 'Koha::Patrons',
95
            value => {
96
                categorycode => $category->categorycode,
97
                password     => Koha::AuthUtils::hash_password('current_password')
98
            }
99
        }
100
    );
101
102
    # Create password history
103
    my $old_password1 = 'old_password_1';
104
    my $old_password2 = 'old_password_2';
105
106
    my $history1 = Koha::PatronPasswordHistory->new(
107
        {
108
            borrowernumber => $patron->borrowernumber,
109
            password       => Koha::AuthUtils::hash_password($old_password1)
110
        }
111
    );
112
    $history1->store;
113
    sleep(1);
114
115
    my $history2 = Koha::PatronPasswordHistory->new(
116
        {
117
            borrowernumber => $patron->borrowernumber,
118
            password       => Koha::AuthUtils::hash_password($old_password2)
119
        }
120
    );
121
    $history2->store;
122
123
    # Test checking against current password
124
    my $is_used = Koha::PatronPasswordHistories->has_used_password(
125
        {
126
            borrowernumber   => $patron->borrowernumber,
127
            password         => 'current_password',
128
            current_password => $patron->password
129
        }
130
    );
131
    ok( $is_used, 'Current password detected as already used' );
132
133
    # Test checking against history
134
    $is_used = Koha::PatronPasswordHistories->has_used_password(
135
        {
136
            borrowernumber   => $patron->borrowernumber,
137
            password         => $old_password1,
138
            current_password => $patron->password
139
        }
140
    );
141
    ok( $is_used, 'Old password from history detected as already used' );
142
143
    $is_used = Koha::PatronPasswordHistories->has_used_password(
144
        {
145
            borrowernumber   => $patron->borrowernumber,
146
            password         => $old_password2,
147
            current_password => $patron->password
148
        }
149
    );
150
    ok( $is_used, 'Another old password from history detected as already used' );
151
152
    # Test new password
153
    $is_used = Koha::PatronPasswordHistories->has_used_password(
154
        {
155
            borrowernumber   => $patron->borrowernumber,
156
            password         => 'brand_new_password',
157
            current_password => $patron->password
158
        }
159
    );
160
    ok( !$is_used, 'New password not detected as used' );
161
162
    # Test with category history_count = 1 (only checks current)
163
    $category->password_history_count(1)->store;
164
165
    $is_used = Koha::PatronPasswordHistories->has_used_password(
166
        {
167
            borrowernumber   => $patron->borrowernumber,
168
            password         => $old_password1,
169
            current_password => $patron->password
170
        }
171
    );
172
    ok( !$is_used, 'With history_count=1, old passwords not checked against history' );
173
174
    $is_used = Koha::PatronPasswordHistories->has_used_password(
175
        {
176
            borrowernumber   => $patron->borrowernumber,
177
            password         => 'current_password',
178
            current_password => $patron->password
179
        }
180
    );
181
    ok( $is_used, 'With history_count=1, current password still checked' );
182
183
    # Test with category history_count = 0 (no checking)
184
    $category->password_history_count(0)->store;
185
186
    $is_used = Koha::PatronPasswordHistories->has_used_password(
187
        {
188
            borrowernumber   => $patron->borrowernumber,
189
            password         => 'current_password',
190
            current_password => $patron->password
191
        }
192
    );
193
    ok( !$is_used, 'With history_count=0, no password checking performed' );
194
195
    # Test fallback to system preference
196
    $category->password_history_count(undef)->store;
197
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 );
198
199
    $is_used = Koha::PatronPasswordHistories->has_used_password(
200
        {
201
            borrowernumber   => $patron->borrowernumber,
202
            password         => $old_password2,
203
            current_password => $patron->password
204
        }
205
    );
206
    ok( $is_used, 'Falls back to system preference when category setting is null' );
207
208
    # Test without current password parameter
209
    $is_used = Koha::PatronPasswordHistories->has_used_password(
210
        {
211
            borrowernumber => $patron->borrowernumber,
212
            password       => $old_password1
213
        }
214
    );
215
    ok( $is_used, 'Can check password history without current_password parameter' );
216
217
    # Test with patron that has no password history
218
    my $new_patron = $builder->build_object(
219
        {
220
            class => 'Koha::Patrons',
221
            value => {
222
                categorycode => $category->categorycode,
223
                password     => Koha::AuthUtils::hash_password('new_patron_password')
224
            }
225
        }
226
    );
227
228
    $is_used = Koha::PatronPasswordHistories->has_used_password(
229
        {
230
            borrowernumber   => $new_patron->borrowernumber,
231
            password         => 'some_password',
232
            current_password => $new_patron->password
233
        }
234
    );
235
    ok( !$is_used, 'Returns false for patron with no password history' );
236
237
    $schema->storage->txn_rollback;
238
};
239
240
subtest 'edge cases and validation' => sub {
241
    plan tests => 4;
242
243
    $schema->storage->txn_begin;
244
245
    # Test with missing parameters
246
    my $is_used = Koha::PatronPasswordHistories->has_used_password( {} );
247
    ok( !$is_used, 'Returns false with no parameters' );
248
249
    $is_used = Koha::PatronPasswordHistories->has_used_password( { borrowernumber => 99999 } );
250
    ok( !$is_used, 'Returns false with missing password' );
251
252
    $is_used = Koha::PatronPasswordHistories->has_used_password( { password => 'test' } );
253
    ok( !$is_used, 'Returns false with missing borrowernumber' );
254
255
    # Test with non-existent patron
256
    $is_used = Koha::PatronPasswordHistories->has_used_password(
257
        {
258
            borrowernumber => 99999,
259
            password       => 'test_password'
260
        }
261
    );
262
    ok( !$is_used, 'Returns false for non-existent patron' );
263
264
    $schema->storage->txn_rollback;
265
};
(-)a/t/db_dependent/Koha/PatronPasswordHistory.t (-1 / +219 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright 2025 Koha Development team
4
#
5
# This file is part of Koha
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use Test::More tests => 4;
23
use Test::NoWarnings;
24
25
use t::lib::TestBuilder;
26
use t::lib::Mocks;
27
28
use Koha::Database;
29
use Koha::PatronPasswordHistory;
30
use Koha::PatronPasswordHistories;
31
32
my $schema  = Koha::Database->new->schema;
33
my $builder = t::lib::TestBuilder->new;
34
35
subtest 'basic functionality' => sub {
36
    plan tests => 3;
37
38
    $schema->storage->txn_begin;
39
40
    my $category = $builder->build_object(
41
        {
42
            class => 'Koha::Patron::Categories',
43
            value => { password_history_count => 3 }
44
        }
45
    );
46
    my $patron = $builder->build_object(
47
        {
48
            class => 'Koha::Patrons',
49
            value => { categorycode => $category->categorycode }
50
        }
51
    );
52
53
    my $password_history = Koha::PatronPasswordHistory->new(
54
        {
55
            borrowernumber => $patron->borrowernumber,
56
            password       => 'plaintext_password'
57
        }
58
    );
59
60
    ok( $password_history, 'PatronPasswordHistory object created' );
61
    isa_ok( $password_history, 'Koha::PatronPasswordHistory' );
62
63
    $password_history->store;
64
    ok( $password_history->in_storage, 'PatronPasswordHistory stored successfully' );
65
66
    $schema->storage->txn_rollback;
67
};
68
69
subtest 'password hashing on store' => sub {
70
    plan tests => 4;
71
72
    $schema->storage->txn_begin;
73
74
    my $category = $builder->build_object(
75
        {
76
            class => 'Koha::Patron::Categories',
77
            value => { password_history_count => 3 }
78
        }
79
    );
80
    my $patron = $builder->build_object(
81
        {
82
            class => 'Koha::Patrons',
83
            value => { categorycode => $category->categorycode }
84
        }
85
    );
86
87
    my $plain_password   = 'test_password_123';
88
    my $password_history = Koha::PatronPasswordHistory->new(
89
        {
90
            borrowernumber => $patron->borrowernumber,
91
            password       => $plain_password
92
        }
93
    );
94
95
    is( $password_history->password, $plain_password, 'Password is plain text before store' );
96
97
    $password_history->store;
98
99
    isnt( $password_history->password, $plain_password, 'Password is hashed after store' );
100
    like( $password_history->password, qr/^\$2a\$/, 'Password is bcrypt hashed' );
101
102
    # Test that already hashed password is not re-hashed
103
    my $hashed_password   = $password_history->password;
104
    my $password_history2 = Koha::PatronPasswordHistory->new(
105
        {
106
            borrowernumber => $patron->borrowernumber,
107
            password       => $hashed_password
108
        }
109
    );
110
111
    $password_history2->store;
112
    is( $password_history2->password, $hashed_password, 'Already hashed password is not re-hashed' );
113
114
    $schema->storage->txn_rollback;
115
};
116
117
subtest 'cleanup of old history entries' => sub {
118
    plan tests => 5;
119
120
    $schema->storage->txn_begin;
121
122
    my $category = $builder->build_object(
123
        {
124
            class => 'Koha::Patron::Categories',
125
            value => { password_history_count => 3 }
126
        }
127
    );
128
    my $patron = $builder->build_object(
129
        {
130
            class => 'Koha::Patrons',
131
            value => { categorycode => $category->categorycode }
132
        }
133
    );
134
135
    # Create multiple password history entries
136
    for my $i ( 1 .. 5 ) {
137
        my $password_history = Koha::PatronPasswordHistory->new(
138
            {
139
                borrowernumber => $patron->borrowernumber,
140
                password       => "password_$i"
141
            }
142
        );
143
        $password_history->store;
144
        sleep(1);    # Ensure different created_on timestamps
145
    }
146
147
    my $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
148
149
    # With password_history_count = 3, we should keep 2 historical entries (3 - 1 for current)
150
    is( $count, 2, 'Old password history entries cleaned up correctly (kept 2 for history_count 3)' );
151
152
    # Test with different category setting - add more entries to test the limit
153
    $category->password_history_count(4)->store;
154
155
    # Add more password entries to exceed the limit
156
    for my $i ( 6 .. 10 ) {
157
        my $password_history = Koha::PatronPasswordHistory->new(
158
            {
159
                borrowernumber => $patron->borrowernumber,
160
                password       => "password_$i"
161
            }
162
        );
163
        $password_history->store;
164
        sleep(1);
165
    }
166
167
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
168
169
    # With password_history_count = 4, we should keep 3 historical entries (4 - 1 for current)
170
    is( $count, 3, 'Cleanup respects updated category password_history_count and does not exceed limit' );
171
172
    # Test with history_count = 1 (should keep 1 historical entry)
173
    $category->password_history_count(1)->store;
174
175
    my $password_history2 = Koha::PatronPasswordHistory->new(
176
        {
177
            borrowernumber => $patron->borrowernumber,
178
            password       => "password_7"
179
        }
180
    );
181
    $password_history2->store;
182
183
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
184
185
    is( $count, 1, 'With history_count=1, keeps exactly 1 entry' );
186
187
    # Test with history_count = 0 (should delete all)
188
    $category->password_history_count(0)->store;
189
190
    my $password_history3 = Koha::PatronPasswordHistory->new(
191
        {
192
            borrowernumber => $patron->borrowernumber,
193
            password       => "password_8"
194
        }
195
    );
196
    $password_history3->store;
197
198
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
199
200
    is( $count, 0, 'With history_count=0, deletes all entries' );
201
202
    # Test fallback to system preference
203
    $category->password_history_count(undef)->store;
204
    t::lib::Mocks::mock_preference( 'PasswordHistoryCount', 2 );
205
206
    my $password_history4 = Koha::PatronPasswordHistory->new(
207
        {
208
            borrowernumber => $patron->borrowernumber,
209
            password       => "password_9"
210
        }
211
    );
212
    $password_history4->store;
213
214
    $count = Koha::PatronPasswordHistories->search( { borrowernumber => $patron->borrowernumber } )->count;
215
216
    is( $count, 1, 'Falls back to system preference when category setting is null' );
217
218
    $schema->storage->txn_rollback;
219
};

Return to bug 40824