Bug 30524: (QA follow-up) Fix tests
[koha.git] / t / db_dependent / Auth.t
1 #!/usr/bin/perl
2 #
3 # This Koha test module is a stub!  
4 # Add more tests here!!!
5
6 use Modern::Perl;
7
8 use CGI qw ( -utf8 );
9
10 use Test::MockObject;
11 use Test::MockModule;
12 use List::MoreUtils qw/all any none/;
13 use Test::More tests => 18;
14 use Test::Warn;
15 use t::lib::Mocks;
16 use t::lib::TestBuilder;
17
18 use C4::Auth;
19 use C4::Members;
20 use Koha::AuthUtils qw( hash_password );
21 use Koha::DateUtils qw( dt_from_string );
22 use Koha::Database;
23 use Koha::Patrons;
24 use Koha::Auth::TwoFactorAuth;
25
26 BEGIN {
27     use_ok('C4::Auth', qw( checkauth haspermission track_login_daily checkpw get_template_and_user checkpw_hash ));
28 }
29
30 my $schema  = Koha::Database->schema;
31 my $builder = t::lib::TestBuilder->new;
32
33 # FIXME: SessionStorage defaults to mysql, but it seems to break transaction
34 # handling
35 t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
36 t::lib::Mocks::mock_preference( 'GDPR_Policy', '' ); # Disabled
37
38 # To silence useless warnings
39 $ENV{REMOTE_ADDR} = '127.0.0.1';
40
41 $schema->storage->txn_begin;
42
43 subtest 'checkauth() tests' => sub {
44
45     plan tests => 8;
46
47     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => undef } });
48
49     # Mock a CGI object with real userid param
50     my $cgi = Test::MockObject->new();
51     $cgi->mock(
52         'param',
53         sub {
54             my $var = shift;
55             if ( $var eq 'userid' ) { return $patron->userid; }
56         }
57     );
58     $cgi->mock( 'cookie', sub { return; } );
59     $cgi->mock( 'request_method', sub { return 'POST' } );
60
61     my $authnotrequired = 1;
62     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, $authnotrequired );
63
64     is( $userid, undef, 'checkauth() returns undef for userid if no logged in user (Bug 18275)' );
65
66     my $db_user_id = C4::Context->config('user');
67     my $db_user_pass = C4::Context->config('pass');
68     $cgi = Test::MockObject->new();
69     $cgi->mock( 'cookie', sub { return; } );
70     $cgi->mock( 'param', sub {
71             my ( $self, $param ) = @_;
72             if ( $param eq 'userid' ) { return $db_user_id; }
73             elsif ( $param eq 'password' ) { return $db_user_pass; }
74             else { return; }
75         });
76     $cgi->mock( 'request_method', sub { return 'POST' } );
77     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, $authnotrequired );
78     is ( $userid, undef, 'If DB user is used, it should not be logged in' );
79
80     my $is_allowed = C4::Auth::haspermission( $db_user_id, { can_do => 'everything' } );
81
82     # FIXME This belongs to t/db_dependent/Auth/haspermission.t but we do not want to c/p the pervious mock statements
83     ok( !$is_allowed, 'DB user should not have any permissions');
84
85     subtest 'Prevent authentication when sending credential via GET' => sub {
86
87         plan tests => 2;
88
89         my $patron = $builder->build_object(
90             { class => 'Koha::Patrons', value => { flags => 1 } } );
91         my $password = set_weak_password($patron);
92         $cgi = Test::MockObject->new();
93         $cgi->mock( 'cookie', sub { return; } );
94         $cgi->mock(
95             'param',
96             sub {
97                 my ( $self, $param ) = @_;
98                 if    ( $param eq 'userid' )   { return $patron->userid; }
99                 elsif ( $param eq 'password' ) { return $password; }
100                 else                           { return; }
101             }
102         );
103
104         $cgi->mock( 'request_method', sub { return 'POST' } );
105         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired' );
106         is( $userid, $patron->userid, 'If librarian user is used and password with POST, they should be logged in' );
107
108         $cgi->mock( 'request_method', sub { return 'GET' } );
109         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired' );
110         is( $userid, undef, 'If librarian user is used and password with GET, they should not be logged in' );
111     };
112
113     subtest 'Template params tests (password_expired)' => sub {
114
115         plan tests => 1;
116
117         my $password_expired;
118
119         my $patron = $builder->build_object( { class => 'Koha::Patrons' } );
120
121         my $password = set_weak_password($patron);
122         $patron->password_expiration_date( dt_from_string->subtract(days => 1) )->store;
123
124         my $cgi_mock = Test::MockModule->new('CGI');
125         $cgi_mock->mock( 'request_method', sub { return 'POST' } );
126         my $cgi  = CGI->new;
127         # Simulating the login form submission
128         $cgi->param( 'userid',   $patron->userid );
129         $cgi->param( 'password', $password );
130
131         my ( $userid, $cookie, $sessionID, $flags, $template ) = C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
132         is( $template->{VARS}->{password_has_expired}, 1 );
133     };
134
135     subtest 'Reset auth state when changing users' => sub {
136
137         #NOTE: It's easiest to detect this when changing to a non-existent user, since
138         #that should trigger a redirect to login (instead of returning a session cookie)
139         plan tests => 2;
140         my $patron = $builder->build_object( { class => 'Koha::Patrons', value => { flags => undef } } );
141
142         my $session = C4::Auth::get_session();
143         $session->param( 'number',    $patron->id );
144         $session->param( 'id',        $patron->userid );
145         $session->param( 'ip',        '1.2.3.4' );
146         $session->param( 'lasttime',  time() );
147         $session->param( 'interface', 'intranet' );
148         $session->flush;
149         my $sessionID = $session->id;
150         C4::Context->_new_userenv($sessionID);
151
152         my ($return) =
153             C4::Auth::check_cookie_auth( $sessionID, undef, { skip_version_check => 1, remote_addr => '1.2.3.4' } );
154         is( $return, 'ok', 'Patron authenticated' );
155
156         my $mock2 = Test::MockModule->new('CGI');
157         $mock2->mock( 'request_method', 'POST' );
158         $mock2->mock( 'cookie',         sub { return $sessionID; } );    # oversimplified..
159         my $cgi = CGI->new;
160
161         $cgi->param( -name => 'userid',             -value => 'Bond' );
162         $cgi->param( -name => 'password',           -value => 'James Bond' );
163         $cgi->param( -name => 'koha_login_context', -value => 1 );
164         my ( $userid, $cookie, $flags, $template );
165         ( $userid, $cookie, $sessionID, $flags, $template ) =
166             C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
167         is( $template->{VARS}->{loginprompt}, 1, 'Changing to non-existent user causes a redirect to login' );
168     };
169
170     subtest 'While still logged in, relogin with another user' => sub {
171         plan tests => 5;
172
173         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => {} });
174         my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => {} });
175         # Create 'former' session
176         my $session = C4::Auth::get_session();
177         $session->param( 'number',       $patron->id );
178         $session->param( 'id',           $patron->userid );
179         $session->param( 'ip',           '1.2.3.4' );
180         $session->param( 'lasttime',     time() );
181         $session->param( 'interface',    'opac' );
182         $session->flush;
183         my $previous_sessionID = $session->id;
184         C4::Context->_new_userenv($previous_sessionID);
185
186         my ( $return ) = C4::Auth::check_cookie_auth( $previous_sessionID, undef, { skip_version_check => 1, remote_addr => '1.2.3.4' } );
187         is( $return, 'ok', 'Former session in shape now' );
188
189         my $mock1 = Test::MockModule->new('C4::Auth');
190         $mock1->mock( 'safe_exit', sub {} );
191         my $mock2 = Test::MockModule->new('CGI');
192         $mock2->mock( 'request_method', 'POST' );
193         $mock2->mock( 'cookie', sub { return $previous_sessionID; } ); # oversimplified..
194         my $cgi = CGI->new;
195         my $password = 'Incr3d1blyZtr@ng93$';
196         $patron2->set_password({ password => $password });
197         $cgi->param( -name => 'userid',             -value => $patron2->userid );
198         $cgi->param( -name => 'password',           -value => $password );
199         $cgi->param( -name => 'koha_login_context', -value => 1 );
200         my ( $userid, $cookie, $sessionID, $flags, $template ) =
201             C4::Auth::checkauth( $cgi, 0, {}, 'opac', undef, undef, { do_not_print => 1 } );
202         is( $userid, $patron2->userid, 'Login of patron2 approved' );
203         isnt( $sessionID, $previous_sessionID, 'Did not return previous session ID' );
204         ok( $sessionID, 'New session ID not empty' );
205
206         # Similar situation: Relogin with former session of $patron, new user $patron2 has no permissions
207         $patron2->flags(undef)->store;
208         $session->param( 'number',       $patron->id );
209         $session->param( 'id',           $patron->userid );
210         $session->param( 'interface',    'intranet' );
211         $session->flush;
212         $previous_sessionID = $session->id;
213         C4::Context->_new_userenv($previous_sessionID);
214         $cgi->param( -name => 'userid',             -value => $patron2->userid );
215         $cgi->param( -name => 'password',           -value => $password );
216         $cgi->param( -name => 'koha_login_context', -value => 1 );
217         ( $userid, $cookie, $sessionID, $flags, $template ) =
218             C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
219         is( $template->{VARS}->{nopermission}, 1, 'No permission response' );
220     };
221
222     subtest 'Two-factor authentication' => sub {
223
224         my $patron = $builder->build_object(
225             { class => 'Koha::Patrons', value => { flags => 1 } } );
226         my $password = 'password';
227         $patron->set_password( { password => $password } );
228         $cgi = Test::MockObject->new();
229
230         my $otp_token;
231         our ( $logout, $sessionID, $verified );
232         $cgi->mock(
233             'param',
234             sub {
235                 my ( $self, $param ) = @_;
236                 if    ( $param eq 'userid' )    { return $patron->userid; }
237                 elsif ( $param eq 'password' )  { return $password; }
238                 elsif ( $param eq 'otp_token' ) { return $otp_token; }
239                 elsif ( $param eq 'logout.x' )  { return $logout; }
240                 else                            { return; }
241             }
242         );
243         $cgi->mock( 'request_method', sub { return 'POST' } );
244         $cgi->mock( 'cookie', sub { return $sessionID } );
245
246         my $two_factor_auth = Test::MockModule->new( 'Koha::Auth::TwoFactorAuth' );
247         $two_factor_auth->mock( 'verify', sub {$verified} );
248
249         my ( $userid, $cookie, $flags );
250         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
251
252         sub logout {
253             my $cgi = shift;
254             $logout = 1;
255             undef $sessionID;
256             C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
257             $logout = 0;
258         }
259
260         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 0 );
261         $patron->auth_method('password')->store;
262         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
263         is( $userid, $patron->userid, 'Succesful login' );
264         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
265         logout($cgi);
266
267         $patron->auth_method('two-factor')->store;
268         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
269         is( $userid, $patron->userid, 'Succesful login' );
270         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
271         logout($cgi);
272
273         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 1 );
274         t::lib::Mocks::mock_config('encryption_key', '1234tH1s=t&st');
275         $patron->auth_method('password')->store;
276         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
277         is( $userid, $patron->userid, 'Succesful login' );
278         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'Second auth not required' );
279         logout($cgi);
280
281         $patron->encode_secret('one_secret');
282         $patron->auth_method('two-factor');
283         $patron->store;
284         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
285         is( $userid, $patron->userid, 'Succesful login' );
286         my $session = C4::Auth::get_session($sessionID);
287         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth required' );
288
289         # Wrong OTP token
290         $otp_token = "wrong";
291         $verified = 0;
292         $patron->auth_method('two-factor')->store;
293         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
294         is( $userid, $patron->userid, 'Succesful login' );
295         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 1, 'Second auth still required after wrong OTP token' );
296
297         $otp_token = "good";
298         $verified = 1;
299         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'intranet' );
300         is( $userid, $patron->userid, 'Succesful login' );
301         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), 0, 'Second auth no longer required if OTP token has been verified' );
302
303         logout($cgi);
304         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 'authrequired', undef, 'opac' );
305         is( $userid, $patron->userid, 'Succesful login at the OPAC' );
306         is( C4::Auth::get_session($sessionID)->param('waiting-for-2FA'), undef, 'No second auth required at the OPAC' );
307
308         t::lib::Mocks::mock_preference( 'TwoFactorAuthentication', 0 );
309     };
310
311     C4::Context->_new_userenv; # For next tests
312
313 };
314
315 subtest 'track_login_daily tests' => sub {
316
317     plan tests => 5;
318
319     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
320     my $userid = $patron->userid;
321
322     $patron->lastseen( undef );
323     $patron->store();
324
325     my $cache     = Koha::Caches->get_instance();
326     my $cache_key = "track_login_" . $patron->userid;
327     $cache->clear_from_cache($cache_key);
328
329     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '1' );
330
331     is( $patron->lastseen, undef, 'Patron should have not last seen when newly created' );
332
333     C4::Auth::track_login_daily( $userid );
334     $patron->_result()->discard_changes();
335     isnt( $patron->lastseen, undef, 'Patron should have last seen set when TrackLastPatronActivity = 1' );
336
337     sleep(1); # We need to wait a tiny bit to make sure the timestamp will be different
338     my $last_seen = $patron->lastseen;
339     C4::Auth::track_login_daily( $userid );
340     $patron->_result()->discard_changes();
341     is( $patron->lastseen, $last_seen, 'Patron last seen should still be unchanged' );
342
343     $cache->clear_from_cache($cache_key);
344     C4::Auth::track_login_daily( $userid );
345     $patron->_result()->discard_changes();
346     isnt( $patron->lastseen, $last_seen, 'Patron last seen should be changed if we cleared the cache' );
347
348     t::lib::Mocks::mock_preference( 'TrackLastPatronActivity', '0' );
349     $patron->lastseen( undef )->store;
350     $cache->clear_from_cache($cache_key);
351     C4::Auth::track_login_daily( $userid );
352     $patron->_result()->discard_changes();
353     is( $patron->lastseen, undef, 'Patron should still have last seen unchanged when TrackLastPatronActivity = 0' );
354
355 };
356
357 subtest 'no_set_userenv parameter tests' => sub {
358
359     plan tests => 7;
360
361     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
362     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
363
364     my $password = set_weak_password($patron);
365
366     ok( checkpw( $patron->userid, $password, undef, undef, 1 ), 'checkpw returns true' );
367     is( C4::Context->userenv, undef, 'Userenv should be undef as required' );
368     C4::Context->_new_userenv('DUMMY SESSION');
369     C4::Context->set_userenv(0,0,0,'firstname','surname', $library->branchcode, 'Library 1', 0, '', '');
370     is( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv gives correct branch' );
371     ok( checkpw( $patron->userid, $password, undef, undef, 1 ), 'checkpw returns true' );
372     is( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv branch is preserved if no_set_userenv is true' );
373     ok( checkpw( $patron->userid, $password, undef, undef, 0 ), 'checkpw still returns true' );
374     isnt( C4::Context->userenv->{branch}, $library->branchcode, 'Userenv branch is overwritten if no_set_userenv is false' );
375 };
376
377 subtest 'checkpw lockout tests' => sub {
378
379     plan tests => 5;
380
381     my $library = $builder->build_object( { class => 'Koha::Libraries' } );
382     my $patron  = $builder->build_object( { class => 'Koha::Patrons' } );
383     my $password = set_weak_password($patron);
384     t::lib::Mocks::mock_preference( 'FailedLoginAttempts', 1 );
385
386     my ( $checkpw, undef, undef ) = checkpw( $patron->cardnumber, $password, undef, undef, 1 );
387     ok( $checkpw, 'checkpw returns true with right password when logging in via cardnumber' );
388     ( $checkpw, undef, undef ) = checkpw( $patron->userid, "wrong_password", undef, undef, 1 );
389     is( $checkpw, 0, 'checkpw returns false when given wrong password' );
390     $patron = $patron->get_from_storage;
391     is( $patron->account_locked, 1, "Account is locked from failed login");
392     ( $checkpw, undef, undef ) = checkpw( $patron->userid, $password, undef, undef, 1 );
393     is( $checkpw, undef, 'checkpw returns undef with right password when account locked' );
394     ( $checkpw, undef, undef ) = checkpw( $patron->cardnumber, $password, undef, undef, 1 );
395     is( $checkpw, undef, 'checkpw returns undefwith right password when logging in via cardnumber if account locked' );
396
397 };
398
399 # get_template_and_user tests
400
401 subtest 'get_template_and_user' => sub {   # Tests for the language URL parameter
402
403     sub MockedCheckauth {
404         my ($query,$authnotrequired,$flagsrequired,$type) = @_;
405         # return vars
406         my $userid = 'cobain';
407         my $sessionID = 234;
408         # we don't need to bother about permissions for this test
409         my $flags = {
410             superlibrarian    => 1, acquisition       => 0,
411             borrowers         => 0,
412             catalogue         => 1, circulate         => 0,
413             coursereserves    => 0, editauthorities   => 0,
414             editcatalogue     => 0,
415             parameters        => 0, permissions       => 0,
416             plugins           => 0, reports           => 0,
417             reserveforothers  => 0, serials           => 0,
418             staffaccess       => 0, tools             => 0,
419             updatecharges     => 0
420         };
421
422         my $session_cookie = $query->cookie(
423             -name => 'CGISESSID',
424             -value    => 'nirvana',
425             -HttpOnly => 1
426         );
427
428         return ( $userid, [ $session_cookie ], $sessionID, $flags );
429     }
430
431     # Mock checkauth, build the scenario
432     my $auth = Test::MockModule->new( 'C4::Auth' );
433     $auth->mock( 'checkauth', \&MockedCheckauth );
434
435     # Make sure 'EnableOpacSearchHistory' is set
436     t::lib::Mocks::mock_preference('EnableOpacSearchHistory',1);
437     # Enable es-ES for the OPAC and staff interfaces
438     t::lib::Mocks::mock_preference('OPACLanguages','en,es-ES');
439     t::lib::Mocks::mock_preference('language','en,es-ES');
440
441     # we need a session cookie
442     $ENV{"SERVER_PORT"} = 80;
443     $ENV{"HTTP_COOKIE"} = 'CGISESSID=nirvana';
444
445     my $query = CGI->new;
446     $query->param('language','es-ES');
447
448     my ( $template, $loggedinuser, $cookies ) = get_template_and_user(
449         {
450             template_name   => "about.tt",
451             query           => $query,
452             type            => "opac",
453             authnotrequired => 1,
454             flagsrequired   => { catalogue => 1 },
455             debug           => 1
456         }
457     );
458
459     ok ( ( all { ref($_) eq 'CGI::Cookie' } @$cookies ),
460             'BZ9735: the cookies array is flat' );
461
462     # new query, with non-existent language (we only have en and es-ES)
463     $query->param('language','tomas');
464
465     ( $template, $loggedinuser, $cookies ) = get_template_and_user(
466         {
467             template_name   => "about.tt",
468             query           => $query,
469             type            => "opac",
470             authnotrequired => 1,
471             flagsrequired   => { catalogue => 1 },
472             debug           => 1
473         }
474     );
475
476     ok( ( none { $_->name eq 'KohaOpacLanguage' and $_->value eq 'tomas' } @$cookies ),
477         'BZ9735: invalid language, it is not set');
478
479     ok( ( any { $_->name eq 'KohaOpacLanguage' and $_->value eq 'en' } @$cookies ),
480         'BZ9735: invalid language, then default to en');
481
482     for my $template_name (
483         qw(
484             ../../../../../../../../../../../../../../../etc/passwd
485             test/../../../../../../../../../../../../../../etc/passwd
486             /etc/passwd
487             test/does_not_finished_by_tt_t
488         )
489     ) {
490         eval {
491             ( $template, $loggedinuser, $cookies ) = get_template_and_user(
492                 {
493                     template_name   => $template_name,
494                     query           => $query,
495                     type            => "intranet",
496                     authnotrequired => 1,
497                     flagsrequired   => { catalogue => 1 },
498                 }
499             );
500         };
501         like ( $@, qr(bad template path), "The file $template_name should not be accessible" );
502     }
503     ( $template, $loggedinuser, $cookies ) = get_template_and_user(
504         {
505             template_name   => 'errors/errorpage.tt',
506             query           => $query,
507             type            => "intranet",
508             authnotrequired => 1,
509             flagsrequired   => { catalogue => 1 },
510         }
511     );
512     my $file_exists = ( -f $template->{filename} ) ? 1 : 0;
513     is ( $file_exists, 1, 'The file errors/errorpage.tt should be accessible (contains integers)' );
514
515     # Regression test for env opac search limit override
516     $ENV{"OPAC_SEARCH_LIMIT"} = "branch:CPL";
517     $ENV{"OPAC_LIMIT_OVERRIDE"} = 1;
518
519     ( $template, $loggedinuser, $cookies) = get_template_and_user(
520         {
521             template_name => 'opac-main.tt',
522             query => $query,
523             type => 'opac',
524             authnotrequired => 1,
525         }
526     );
527     is($template->{VARS}->{'opac_name'}, "CPL", "Opac name was set correctly");
528     is($template->{VARS}->{'opac_search_limit'}, "branch:CPL", "Search limit was set correctly");
529
530     $ENV{"OPAC_SEARCH_LIMIT"} = "branch:multibranch-19";
531
532     ( $template, $loggedinuser, $cookies) = get_template_and_user(
533         {
534             template_name => 'opac-main.tt',
535             query => $query,
536             type => 'opac',
537             authnotrequired => 1,
538         }
539     );
540     is($template->{VARS}->{'opac_name'}, "multibranch-19", "Opac name was set correctly");
541     is($template->{VARS}->{'opac_search_limit'}, "branch:multibranch-19", "Search limit was set correctly");
542
543     delete $ENV{"HTTP_COOKIE"};
544 };
545
546 # Check that there is always an OPACBaseURL set.
547 my $input = CGI->new();
548 my ( $template1, $borrowernumber, $cookie );
549 ( $template1, $borrowernumber, $cookie ) = get_template_and_user(
550     {
551         template_name => "opac-detail.tt",
552         type => "opac",
553         query => $input,
554         authnotrequired => 1,
555     }
556 );
557
558 ok( ( any { 'OPACBaseURL' eq $_ } keys %{$template1->{VARS}} ),
559     'OPACBaseURL is in OPAC template' );
560
561 my ( $template2 );
562 ( $template2, $borrowernumber, $cookie ) = get_template_and_user(
563     {
564         template_name => "catalogue/detail.tt",
565         type => "intranet",
566         query => $input,
567         authnotrequired => 1,
568     }
569 );
570
571 ok( ( any { 'OPACBaseURL' eq $_ } keys %{$template2->{VARS}} ),
572     'OPACBaseURL is in Staff template' );
573
574 my $hash1 = hash_password('password');
575 my $hash2 = hash_password('password');
576
577 ok(C4::Auth::checkpw_hash('password', $hash1), 'password validates with first hash');
578 ok(C4::Auth::checkpw_hash('password', $hash2), 'password validates with second hash');
579
580 subtest 'Check value of login_attempts in checkpw' => sub {
581     plan tests => 11;
582
583     t::lib::Mocks::mock_preference('FailedLoginAttempts', 3);
584
585     # Only interested here in regular login
586     $C4::Auth::cas  = 0;
587     $C4::Auth::ldap = 0;
588
589     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
590     $patron->login_attempts(2);
591     $patron->password('123')->store; # yes, deliberately not hashed
592
593     is( $patron->account_locked, 0, 'Patron not locked' );
594     my @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
595         # Note: 123 will not be hashed to 123 !
596     is( $test[0], 0, 'checkpw should have failed' );
597     $patron->discard_changes; # refresh
598     is( $patron->login_attempts, 3, 'Login attempts increased' );
599     is( $patron->account_locked, 1, 'Check locked status' );
600
601     # And another try to go over the limit: different return value!
602     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
603     is( @test, 0, 'checkpw failed again and returns nothing now' );
604     $patron->discard_changes; # refresh
605     is( $patron->login_attempts, 3, 'Login attempts not increased anymore' );
606
607     # Administrative lockout cannot be undone?
608     # Pass the right password now (or: add a nice mock).
609     my $auth = Test::MockModule->new( 'C4::Auth' );
610     $auth->mock( 'checkpw_hash', sub { return 1; } ); # not for production :)
611     $patron->login_attempts(0)->store;
612     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
613     is( $test[0], 1, 'Build confidence in the mock' );
614     $patron->login_attempts(-1)->store;
615     is( $patron->account_locked, 1, 'Check administrative lockout' );
616     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
617     is( @test, 0, 'checkpw gave red' );
618     $patron->discard_changes; # refresh
619     is( $patron->login_attempts, -1, 'Still locked out' );
620     t::lib::Mocks::mock_preference('FailedLoginAttempts', ''); # disable
621     is( $patron->account_locked, 1, 'Check administrative lockout without pref' );
622 };
623
624 subtest 'Check value of login_attempts in checkpw' => sub {
625     plan tests => 2;
626
627     t::lib::Mocks::mock_preference('FailedLoginAttempts', 3);
628     my $patron = $builder->build_object({ class => 'Koha::Patrons' });
629     $patron->set_password({ password => '123', skip_validation => 1 });
630
631     my @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
632     is( $test[0], 1, 'Patron authenticated correctly' );
633
634     $patron->password_expiration_date('2020-01-01')->store;
635     @test = checkpw( $patron->userid, '123', undef, 'opac', 1 );
636     is( $test[0], -2, 'Patron returned as expired correctly' );
637
638 };
639
640 subtest '_timeout_syspref' => sub {
641
642     plan tests => 6;
643
644     t::lib::Mocks::mock_preference('timeout', "100");
645     is( C4::Auth::_timeout_syspref, 100, );
646
647     t::lib::Mocks::mock_preference('timeout', "2d");
648     is( C4::Auth::_timeout_syspref, 2*86400, );
649
650     t::lib::Mocks::mock_preference('timeout', "2D");
651     is( C4::Auth::_timeout_syspref, 2*86400, );
652
653     t::lib::Mocks::mock_preference('timeout', "10h");
654     is( C4::Auth::_timeout_syspref, 10*3600, );
655
656     t::lib::Mocks::mock_preference('timeout', "10x");
657     warning_is
658         { is( C4::Auth::_timeout_syspref, 600, ); }
659         "The value of the system preference 'timeout' is not correct, defaulting to 600",
660         'Bad values throw a warning and fallback to 600';
661 };
662
663 subtest 'check_cookie_auth' => sub {
664     plan tests => 4;
665
666     t::lib::Mocks::mock_preference('timeout', "1d"); # back to default
667
668     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 } });
669
670     # Mock a CGI object with real userid param
671     my $cgi = Test::MockObject->new();
672     $cgi->mock(
673         'param',
674         sub {
675             my $var = shift;
676             if ( $var eq 'userid' ) { return $patron->userid; }
677         }
678     );
679     $cgi->mock('multi_param', sub {return q{}} );
680     $cgi->mock( 'cookie', sub { return; } );
681     $cgi->mock( 'request_method', sub { return 'POST' } );
682
683     $ENV{REMOTE_ADDR} = '127.0.0.1';
684
685     # Setting authnotrequired=1 or we wont' hit the return but the end of the sub that prints headers
686     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 1 );
687
688     my ($auth_status, $session) = C4::Auth::check_cookie_auth($sessionID);
689     isnt( $auth_status, 'ok', 'check_cookie_auth should not return ok if the user has not been authenticated before if no permissions needed' );
690     is( $auth_status, 'anon', 'check_cookie_auth should return anon if the user has not been authenticated before and no permissions needed' );
691
692     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 1 );
693
694     ($auth_status, $session) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1});
695     isnt( $auth_status, 'ok', 'check_cookie_auth should not return ok if the user has not been authenticated before and permissions needed' );
696     is( $auth_status, 'anon', 'check_cookie_auth should return anon if the user has not been authenticated before and permissions needed' );
697
698     #FIXME We should have a test to cover 'failed' status when a user has logged in, but doesn't have permission
699 };
700
701 subtest 'checkauth & check_cookie_auth' => sub {
702     plan tests => 30;
703
704     # flags = 4 => { catalogue => 1 }
705     my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 4 } });
706     my $password = set_weak_password($patron);
707
708     my $cgi_mock = Test::MockModule->new('CGI');
709     $cgi_mock->mock( 'request_method', sub { return 'POST' } );
710
711     my $cgi = CGI->new;
712
713     my $auth = Test::MockModule->new( 'C4::Auth' );
714     # Tests will fail if we hit safe_exit
715     $auth->mock( 'safe_exit', sub { return } );
716
717     my ( $userid, $cookie, $sessionID, $flags );
718     {
719         # checkauth will redirect and safe_exit if not authenticated and not authorized
720         local *STDOUT;
721         my $stdout;
722         open STDOUT, '>', \$stdout;
723         C4::Auth::checkauth($cgi, 0, {catalogue => 1});
724         like( $stdout, qr{<title>\s*Log in to your account} );
725         $sessionID = ( $stdout =~ m{Set-Cookie: CGISESSID=((\d|\w)+);} ) ? $1 : undef;
726         ok($sessionID);
727         close STDOUT;
728     };
729
730     my $first_sessionID = $sessionID;
731
732     $ENV{"HTTP_COOKIE"} = "CGISESSID=$sessionID";
733     # Not authenticated yet, the login form is displayed
734     my $template;
735     ( $userid, $cookie, $sessionID, $flags, $template ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1}, 'intranet', undef, undef, { do_not_print => 1 } );
736     is( $template->{VARS}->{loginprompt}, 1, );
737
738     # Sending undefined fails obviously
739     my ( $auth_status, $session ) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1} );
740     is( $auth_status, 'failed' );
741     is( $session, undef );
742
743     # Simulating the login form submission
744     $cgi->param('userid', $patron->userid);
745     $cgi->param('password', $password);
746
747     # Logged in!
748     ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
749     is( $sessionID, $first_sessionID );
750     is( $userid, $patron->userid );
751
752     ( $auth_status, $session ) = C4::Auth::check_cookie_auth($sessionID, {catalogue => 1});
753     is( $auth_status, 'ok' );
754     is( $session->id, $first_sessionID );
755
756     # Logging out!
757     $cgi->param('logout.x', 1);
758     $cgi->delete( 'userid', 'password' );
759     ( $userid, $cookie, $sessionID, $flags, $template ) =
760         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
761
762     is( $sessionID, undef );
763     is( $ENV{"HTTP_COOKIE"}, "CGISESSID=$first_sessionID", 'HTTP_COOKIE not unset' );
764     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, {catalogue => 1} );
765     is( $auth_status, "expired");
766     is( $session, undef );
767
768     {
769         # Trying to access without sessionID
770         $cgi = CGI->new;
771         ( $auth_status, $session) = C4::Auth::check_cookie_auth(undef, {catalogue => 1});
772         is( $auth_status, 'failed' );
773         is( $session, undef );
774
775         # This will fail on permissions
776         undef $ENV{"HTTP_COOKIE"};
777         {
778             local *STDOUT;
779             my $stdout;
780             open STDOUT, '>', \$stdout;
781             ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1} );
782             close STDOUT;
783         }
784         is( $userid, undef );
785         is( $sessionID, undef );
786     }
787
788     {
789         # First logging in
790         $cgi = CGI->new;
791         $cgi->param('userid', $patron->userid);
792         $cgi->param('password', $password);
793         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
794         is( $userid, $patron->userid );
795         $first_sessionID = $sessionID;
796
797         # Patron does not have the borrowers permission
798         # $ENV{"HTTP_COOKIE"} = "CGISESSID=$sessionID"; # not needed, we use $cgi here
799         {
800             local *STDOUT;
801             my $stdout;
802             open STDOUT, '>', \$stdout;
803             ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {borrowers => 1} );
804             close STDOUT;
805         }
806         is( $userid, undef );
807         is( $sessionID, undef );
808
809         # When calling check_cookie_auth, the session will be deleted
810         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, { borrowers => 1 } );
811         is( $auth_status, "failed" );
812         is( $session, undef );
813         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $first_sessionID, { borrowers => 1 } );
814         is( $auth_status, 'expired', 'Session no longer exists' );
815
816         # NOTE: It is not what the UI is doing.
817         # From the UI we are allowed to hit an unauthorized page then reuse the session to hit back authorized area.
818         # It is because check_cookie_auth is ALWAYS called from checkauth WITHOUT $flagsrequired
819         # It then return "ok", when the previous called got "failed"
820
821         # Try reusing the deleted session: since it does not exist, we should get a new one now when passing correct permissions
822         $cgi->cookie( -name => 'CGISESSID', value => $first_sessionID );
823         ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 0, {catalogue => 1});
824         is( $userid, $patron->userid );
825         isnt( $sessionID, undef, 'Check if we have a sessionID' );
826         isnt( $sessionID, $first_sessionID, 'New value expected' );
827         ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID, {catalogue => 1} );
828         is( $auth_status, "ok" );
829         is( $session->id, $sessionID, 'Same session' );
830         # Two additional tests on userenv
831         is( $C4::Context::context->{activeuser}, $session->id, 'Check if environment has been setup for session' );
832         is( C4::Context->userenv->{id}, $userid, 'Check userid in userenv' );
833     }
834 };
835
836 subtest 'Userenv clearing in check_cookie_auth' => sub {
837     # Note: We did already test userenv for a logged-in user in previous subtest
838     plan tests => 9;
839
840     t::lib::Mocks::mock_preference( 'timeout', 600 );
841     my $cgi = CGI->new;
842
843     # Create a new anonymous session by passing a fake session ID
844     $cgi->cookie( -name => 'CGISESSID', -value => 'fake_sessionID' );
845     my ($userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth($cgi, 1);
846     my ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
847     is( $auth_status, 'anon', 'Should be anonymous' );
848     is( $C4::Context::context->{activeuser}, $session->id, 'Check activeuser' );
849     is( defined C4::Context->userenv, 1, 'There should be a userenv' );
850     is(  C4::Context->userenv->{id}, q{}, 'userid should be empty string' );
851
852     # Make the session expire now, check_cookie_auth will delete it
853     $session->param('lasttime', time() - 1200 );
854     $session->flush;
855     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
856     is( $auth_status, 'expired', 'Should be expired' );
857     is( C4::Context->userenv, undef, 'Environment should be cleared too' );
858
859     # Show that we clear the userenv again: set up env and check deleted session
860     C4::Context->_new_userenv( $sessionID );
861     C4::Context->set_userenv; # empty
862     is( defined C4::Context->userenv, 1, 'There should be an empty userenv again' );
863     ( $auth_status, $session) = C4::Auth::check_cookie_auth( $sessionID );
864     is( $auth_status, 'expired', 'Should be expired already' );
865     is( C4::Context->userenv, undef, 'Environment should be cleared again' );
866 };
867
868 $schema->storage->txn_rollback;
869
870 subtest 'checkpw() return values tests' => sub {
871
872     plan tests => 3;
873
874     subtest 'Internal check tests' => sub {
875
876         plan tests => 25;
877
878         $schema->storage->txn_begin;
879
880         my $account_locked;
881         my $password_expired;
882
883         my $mock_patron = Test::MockModule->new('Koha::Patron');
884         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
885         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
886
887         # Only interested here in regular login
888         t::lib::Mocks::mock_config( 'useshibboleth', undef );
889         $C4::Auth::cas  = 0;
890         $C4::Auth::ldap = 0;
891
892         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
893         my $password = set_weak_password($patron);
894
895         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
896         my $unused_userid     = $patron_to_delete->userid;
897         my $unused_cardnumber = $patron_to_delete->cardnumber;
898         $patron_to_delete->delete;
899
900         $account_locked = 1;
901         my @return = checkpw( $patron->userid, $password, undef, );
902         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
903
904         $account_locked = 0;
905
906         my @matchpoints = qw(userid cardnumber);
907         foreach my $matchpoint (@matchpoints) {
908
909             @return = checkpw( $patron->$matchpoint, $password, undef, );
910
911             is( $return[0],        1,                   "Password validation successful returns 1 ($matchpoint)" );
912             is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
913             is( $return[2],        $patron->userid,     '`userid` returned' );
914             is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
915             is( $return[3]->id,    $patron->id,         'Correct patron returned' );
916         }
917
918         @return = checkpw( $patron->userid, $password . 'hey', undef, );
919
920         is( scalar @return,    2, "Two results on invalid password scenario" );
921         is( $return[0],        0, '0 returned on invalid password' );
922         is( ref( $return[1] ), 'Koha::Patron' );
923         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
924
925         $password_expired = 1;
926         @return           = checkpw( $patron->userid, $password, undef, );
927
928         is( scalar @return,    2,  "Two results on expired password scenario" );
929         is( $return[0],        -2, '-2 returned' );
930         is( ref( $return[1] ), 'Koha::Patron' );
931         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
932
933         @return = checkpw( $unused_userid, $password, undef, );
934
935         is( scalar @return, 2,     "Two results on non-existing userid scenario" );
936         is( $return[0],     0,     '0 returned' );
937         is( $return[1],     undef, 'Undef returned, representing no match' );
938
939         @return = checkpw( $unused_cardnumber, $password, undef, );
940
941         is( scalar @return, 2,     "Only one result on non-existing cardnumber scenario" );
942         is( $return[0],     0,     '0 returned' );
943         is( $return[1],     undef, 'Undef returned, representing no match' );
944
945         $schema->storage->txn_rollback;
946     };
947
948     subtest 'CAS check (mocked) tests' => sub {
949
950         plan tests => 25;
951
952         $schema->storage->txn_begin;
953
954         my $account_locked;
955         my $password_expired;
956
957         my $mock_patron = Test::MockModule->new('Koha::Patron');
958         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
959         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
960
961         # Only interested here in regular login
962         t::lib::Mocks::mock_config( 'useshibboleth', undef );
963         $C4::Auth::cas  = 1;
964         $C4::Auth::ldap = 0;
965
966         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
967         my $password = 'thePassword123';
968         $patron->set_password( { password => $password, skip_validation => 1 } );
969
970         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
971         my $unused_userid     = $patron_to_delete->userid;
972         my $unused_cardnumber = $patron_to_delete->cardnumber;
973         $patron_to_delete->delete;
974
975         my $ticket = '123456';
976         my $query  = CGI->new;
977         $query->param( -name => 'ticket', -value => $ticket );
978
979         my @cas_return = ( 1, $patron->cardnumber, $patron->userid, $ticket, Koha::Patrons->find( $patron->id ) );
980
981         my $cas_mock = Test::MockModule->new('C4::Auth');
982         $cas_mock->mock( 'checkpw_cas', sub { return @cas_return; } );
983
984         $account_locked = 1;
985         my @return = checkpw( $patron->userid, $password, $query, );
986         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
987
988         $account_locked = 0;
989
990         my @matchpoints = qw(userid cardnumber);
991         foreach my $matchpoint (@matchpoints) {
992
993             @return = checkpw( $patron->$matchpoint, $password, $query, );
994
995             is( $return[0],        1,                   "Password validation successful returns 1 ($matchpoint)" );
996             is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
997             is( $return[2],        $patron->userid,     '`userid` returned' );
998             is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
999             is( $return[3]->id,    $patron->id,         'Correct patron returned' );
1000         }
1001
1002         @return = checkpw( $patron->userid, $password . 'hey', $query, );
1003
1004         is( scalar @return,    2, "Two results on invalid password scenario" );
1005         is( $return[0],        0, '0 returned on invalid password' );
1006         is( ref( $return[1] ), 'Koha::Patron' );
1007         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1008
1009         $password_expired = 1;
1010         @return           = checkpw( $patron->userid, $password, $query, );
1011
1012         is( scalar @return,    2,  "Two results on expired password scenario" );
1013         is( $return[0],        -2, '-2 returned' );
1014         is( ref( $return[1] ), 'Koha::Patron' );
1015         is( $return[1]->id,    $patron->id, 'Patron matched correctly' );
1016
1017         @return = checkpw( $unused_userid, $password, $query, );
1018
1019         is( scalar @return, 2,     "Two results on non-existing userid scenario" );
1020         is( $return[0],     0,     '0 returned' );
1021         is( $return[1],     undef, 'Undef returned, representing no match' );
1022
1023         @return = checkpw( $unused_cardnumber, $password, $query, );
1024
1025         is( scalar @return, 2,     "Only one result on non-existing cardnumber scenario" );
1026         is( $return[0],     0,     '0 returned' );
1027         is( $return[1],     undef, 'Undef returned, representing no match' );
1028
1029         $schema->storage->txn_rollback;
1030     };
1031
1032     subtest 'Shibboleth check (mocked) tests' => sub {
1033
1034         plan tests => 6;
1035
1036         $schema->storage->txn_begin;
1037
1038         my $account_locked;
1039         my $password_expired;
1040
1041         my $mock_patron = Test::MockModule->new('Koha::Patron');
1042         $mock_patron->mock( 'account_locked',   sub { return $account_locked; } );
1043         $mock_patron->mock( 'password_expired', sub { return $password_expired; } );
1044
1045         # Only interested here in regular login
1046         t::lib::Mocks::mock_config( 'useshibboleth', 1 );
1047         $C4::Auth::cas  = 0;
1048         $C4::Auth::ldap = 0;
1049
1050         my $patron   = $builder->build_object( { class => 'Koha::Patrons' } );
1051         my $password = 'thePassword123';
1052         $patron->set_password( { password => $password, skip_validation => 1 } );
1053
1054         my $patron_to_delete  = $builder->build_object( { class => 'Koha::Patrons' } );
1055         my $unused_userid     = $patron_to_delete->userid;
1056         my $unused_cardnumber = $patron_to_delete->cardnumber;
1057         $patron_to_delete->delete;
1058
1059         my @shib_return = ( 1, $patron->cardnumber, $patron->userid, Koha::Patrons->find( $patron->id ) );
1060
1061         my $auth_mock = Test::MockModule->new('C4::Auth');
1062         $auth_mock->mock( 'shib_ok',        1 );
1063         $auth_mock->mock( 'get_login_shib', 1 );
1064
1065         my $shib_mock = Test::MockModule->new('C4::Auth_with_shibboleth');
1066         $shib_mock->mock( 'checkpw_shib', sub { return @shib_return; } );
1067
1068         $account_locked = 1;
1069         my @return = checkpw( $patron->userid );
1070         is_deeply( \@return, [], 'If the account is locked, empty list is returned' );
1071
1072         $account_locked = 0;
1073
1074         @return = checkpw();
1075
1076         is( $return[0],        1,                   "Password validation successful returns 1" );
1077         is( $return[1],        $patron->cardnumber, '`cardnumber` returned' );
1078         is( $return[2],        $patron->userid,     '`userid` returned' );
1079         is( ref( $return[3] ), 'Koha::Patron',      'Koha::Patron object reference returned' );
1080         is( $return[3]->id,    $patron->id,         'Correct patron returned' );
1081
1082         $schema->storage->txn_rollback;
1083     };
1084 };
1085
1086 subtest 'AutoLocation' => sub {
1087
1088     plan tests => 7;
1089
1090     $schema->storage->txn_begin;
1091
1092     t::lib::Mocks::mock_preference( 'AutoLocation', 0 );
1093
1094     my $patron   = $builder->build_object( { class => 'Koha::Patrons', value => { flags => 1 } } );
1095     my $password = 'password';
1096     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1097     $patron->set_password( { password => $password } );
1098
1099     my $cgi_mock = Test::MockModule->new('CGI');
1100     $cgi_mock->mock( 'request_method', sub { return 'POST' } );
1101     my $cgi  = CGI->new;
1102     my $auth = Test::MockModule->new('C4::Auth');
1103
1104     # Simulating the login form submission
1105     $cgi->param( 'userid',   $patron->userid );
1106     $cgi->param( 'password', $password );
1107
1108     $ENV{REMOTE_ADDR} = '127.0.0.1';
1109     my ( $userid, $cookie, $sessionID, $flags ) = C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1110     is( $userid, $patron->userid );
1111
1112     my $template;
1113     t::lib::Mocks::mock_preference( 'AutoLocation', 1 );
1114
1115     # AutoLocation: "Require staff to log in from a computer in the IP address range specified by their library (if any)"
1116     $patron->library->branchip('')->store;    # There is none, allow access from anywhere
1117     ( $userid, $cookie, $sessionID, $flags, $template ) =
1118         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1119     is( $userid,   $patron->userid );
1120     is( $template, undef );
1121
1122     $patron->library->branchip('1.2.3.4')->store;
1123     ( $userid, $cookie, $sessionID, $flags, $template ) =
1124         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet', undef, undef, { do_not_print => 1 } );
1125     is( $template->{VARS}->{wrongip}, 1 );
1126
1127     $patron->library->branchip('127.0.0.1')->store;
1128     ( $userid, $cookie, $sessionID, $flags, $template ) =
1129         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1130     is( $userid,   $patron->userid );
1131     is( $template, undef );
1132
1133     my $other_library = $builder->build_object( { class => 'Koha::Libraries', value => { branchip => '127.0.0.1' } } );
1134     $patron->library->branchip('127.0.0.1')->store;
1135     ( $userid, $cookie, $sessionID, $flags, $template ) =
1136         C4::Auth::checkauth( $cgi, 0, { catalogue => 1 }, 'intranet' );
1137     my $session = C4::Auth::get_session($sessionID);
1138     is( $session->param('branch'), $patron->branchcode );
1139
1140     $schema->storage->txn_rollback;
1141
1142 };
1143
1144 sub set_weak_password {
1145     my ($patron) = @_;
1146     my $password = 'password';
1147     t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
1148     $patron->set_password( { password => $password } );
1149     return $password;
1150 }