Bug 34847: Fix t/db_dependent/Search.t
[koha.git] / t / db_dependent / selenium / authentication.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Copyright (C) 2017  Catalyst IT
6 # Copyright 2021 Koha Development team
7 #
8 # Koha is free software; you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # Koha is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20
21 #This selenium test is to test authentication, by performing the following: create a category and patron (same as basic_workflow.t). Then the superlibrarian logs out and the created patron must log into the staff intranet and OPAC
22
23 #Note: If you are testing this on kohadevbox with selenium installed in kohadevbox then you need to set the staffClientBaseURL to localhost:8080 and the OPACBaseURL to localhost:80
24
25 use Modern::Perl;
26 use Test::More tests => 3;
27
28 use C4::Context;
29 use Koha::AuthUtils;
30 use t::lib::Mocks;
31 use t::lib::Selenium;
32 use t::lib::TestBuilder;
33
34 my @data_to_cleanup;
35
36 SKIP: {
37     eval { require Selenium::Remote::Driver; };
38     skip "Selenium::Remote::Driver is needed for selenium tests.", 3 if $@;
39
40     my $builder  = t::lib::TestBuilder->new;
41     my $s        = t::lib::Selenium->new;
42     my $driver   = $s->driver;
43
44     subtest 'Staff interface authentication' => sub {
45         plan tests => 6;
46         my $mainpage = $s->base_url . q|mainpage.pl|;
47         $driver->get($mainpage);
48         like( $driver->get_title, qr(Log in to Koha), 'Hitting the main page should redirect to the login form');
49
50         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 0 }});
51         my $password = Koha::AuthUtils::generate_password($patron->category);
52         t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
53         $patron->set_password({ password => $password });
54
55         # Patron does not have permission to access staff interface
56         $s->auth( $patron->userid, $password );
57         like( $driver->get_title, qr(Access denied), 'Patron without permission should be redirected to the login form' );
58
59         $driver->get($mainpage . q|?logout.x=1|);
60         $patron->flags(4)->store; # catalogue permission
61         $s->auth( $patron->userid, $password );
62         like( $driver->get_title, qr(Koha staff interface), 'Patron with flags catalogue should be able to login' );
63
64         $driver->get($mainpage . q|?logout.x=1|);
65         like( $driver->get_title(), qr(Log in to Koha), 'If logout is requested, login form should be displayed' );
66
67         $patron->flags(1)->store; # superlibrarian permission
68         $s->auth( $patron->userid, $password );
69         like( $driver->get_title, qr(Koha staff interface), 'Patron with flags superlibrarian should be able to login' );
70
71         subtest 'not authorized' => sub {
72             plan tests => 17;
73
74             # First, logout!
75             $driver->get($mainpage . q|?logout.x=1|);
76             $patron->flags(4)->store; # Patron has only catalogue permission
77             like( $driver->get_title, qr(Log in to Koha), 'Patron should hit the login form after logout' );
78             # Login!
79             $s->fill_form({ userid => $patron->userid, password => $password });
80             $s->driver->find_element('//input[@id="submit-button"]')->click;
81
82             my $cookie = $driver->get_cookie_named('CGISESSID');
83             my $first_sessionID = $cookie->{value};
84
85             # Patron is logged in and got a CGISESSID cookie, miam
86             like( $driver->get_title, qr(Koha staff interface), 'Patron is logged in' );
87             $cookie = $driver->get_cookie_named('CGISESSID');
88             is( $cookie->{value}, $first_sessionID, 'no new session after login, the session has been upgraded' );
89
90             # Authorized page can be accessed, cookie does not change
91             $driver->get( $s->base_url . q|catalogue/search.pl| );
92             like( $driver->get_title, qr(Advanced search), 'Patron can access advanced search' );
93             $cookie = $driver->get_cookie_named('CGISESSID');
94             is( $cookie->{value}, $first_sessionID, 'no new session after hit' );
95
96             # Unauthorized page redirect to the login form
97             $driver->get( $s->base_url . q|circ/circulation.pl| );
98             like( $driver->get_title, qr(Access denied), 'Patron cannot access the circulation module' );
99             # But the patron does not lose the CGISESSID cookie!
100             $cookie = $driver->get_cookie_named('CGISESSID');
101             is( $cookie->{value}, $first_sessionID, 'no new session if unauthorized page is hit' );
102
103             # Luckily mainpage can still be accessed
104             $s->click( { id => 'mainpage', main_class => 'main container-fluid' } );
105             like( $driver->get_title, qr(Koha staff interface), 'Patron can come back to the mainpage' );
106             $cookie = $driver->get_cookie_named('CGISESSID');
107             is( $cookie->{value}, $first_sessionID, 'no new session if back to the mainpage' );
108
109             # As well as the search
110             $driver->get( $s->base_url . q|catalogue/search.pl| );
111             like( $driver->get_title, qr(Advanced search), 'Patron can access advanced search' );
112             # But circulation module is prohibided!
113             $driver->get( $s->base_url . q|circ/circulation.pl| );
114             like( $driver->get_title, qr(Access denied), 'Patron cannot access the circulation module' );
115             # Still can reuse the same cookie
116             $cookie = $driver->get_cookie_named('CGISESSID');
117             is( $cookie->{value}, $first_sessionID, 'no new session if unauthorized page is hit' );
118
119             # This is the "previous page" using the back() JS
120             $s->click( { id => 'previous_page', main_class => 'main container-fluid' } );
121             like( $driver->get_title, qr(Advanced search), 'Patron can come back to the previous page' );
122             $cookie = $driver->get_cookie_named('CGISESSID');
123             is( $cookie->{value}, $first_sessionID, 'no new session if back to the previous page' );
124
125             # Check with a script that is using check_cookie_auth, session must not be deleted!
126             $driver->get( $s->base_url . q|svc/checkouts| );
127             #FIXME - 500 is the current behaviour, but it's not nice. It could be improved.
128             like( $driver->get_title, qr(Error 500), 'Patron cannot access svc script where circulate permissions are required');
129             $driver->get( $s->base_url . q|catalogue/search.pl| );
130             like( $driver->get_title, qr(Advanced search), 'Patron can reuse the cookie after a script that used check_cookie_auth' );
131             $cookie = $driver->get_cookie_named('CGISESSID');
132             is( $cookie->{value}, $first_sessionID, 'no new session if unauthorized page is hit' );
133         };
134         push @data_to_cleanup, $patron, $patron->category, $patron->library;
135     };
136
137     subtest 'OPAC interface authentication' => sub {
138         plan tests => 7;
139
140         my $mainpage = $s->opac_base_url . q|opac-main.pl|;
141
142         $driver->get($mainpage . q|?logout.x=1|); # Disconnect first! We are logged in if staff and opac interfaces are separated by ports
143
144         $driver->get($mainpage);
145         like( $driver->get_title, qr(Koha online catalog), 'Hitting the main page should not redirect to the login form');
146
147         my $patron = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 0 }});
148         my $password = Koha::AuthUtils::generate_password($patron->category);
149         t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
150         $patron->set_password({ password => $password });
151
152         # Using the modal
153         $driver->find_element('//a[@class="nav-link login-link loginModal-trigger"]')->click;
154         $s->fill_form( { muserid => $patron->userid, mpassword => $password } );
155         $driver->find_element('//div[@id="loginModal"]//input[@type="submit"]')->click;
156         like( $driver->get_title, qr(Koha online catalog), 'Patron without permission should be able to login to the OPAC using the modal' );
157         $driver->find_element('//div[@id="userdetails"]');
158         like( $driver->get_title, qr(Your library home), 'Patron without permissions should be able to login to the OPAC using the modal');
159
160         $driver->find_element('//a[@id="user-menu"]')->click;
161         $driver->find_element('//a[@id="logout"]')->click;
162         $driver->get($mainpage); # This should not be needed but we the next find_element fails randomly
163
164         { # Temporary debug
165             $driver->error_handler(
166                 sub {
167                     my ( $driver, $selenium_error ) = @_;
168                     print STDERR "\nSTRACE:";
169                     my $i = 1;
170                     while ( (my @call_details = (caller($i++))) ){
171                         print STDERR "\t" . $call_details[1]. ":" . $call_details[2] . " in " . $call_details[3]."\n";
172                     }
173                     print STDERR "\n";
174                     print STDERR sprintf("Is logged in patron: %s (%s)?\n", $patron->firstname, $patron->surname );
175                     $s->capture( $driver );
176                     croak $selenium_error;
177                 }
178             );
179
180             $driver->find_element('//div[@id="login"]'); # logged out
181             $s->add_error_handler; # Reset to the default error handler
182         }
183
184         # Using the form on the right
185         $s->fill_form( { userid => $patron->userid, password => $password } );
186         $s->submit_form;
187         $driver->find_element('//div[@id="userdetails"]');
188         like( $driver->get_title, qr(Your library home), 'Patron without permissions should be able to login to the OPAC using the form on the right');
189
190         $driver->find_element('//a[@id="user-menu"]')->click;
191         $driver->find_element('//a[@id="logout"]')->click;
192         $driver->find_element('//div[@id="login"]'); # logged out
193
194
195         $patron->flags(4)->store; # catalogue permission
196         $s->fill_form( { userid => $patron->userid, password => $password } );
197         $s->submit_form;
198         $driver->find_element('//div[@id="userdetails"]');
199         like( $driver->get_title, qr(Your library home), 'Patron with catalogue permission should be able to login to the OPAC');
200
201         $driver->find_element('//a[@id="user-menu"]')->click;
202         $driver->find_element('//a[@id="logout"]')->click;
203         $driver->find_element('//div[@id="login"]'); # logged out
204
205         $patron->flags(1)->store; # superlibrarian permission
206         $s->fill_form( { userid => $patron->userid, password => $password } );
207         $s->submit_form;
208         $driver->find_element('//div[@id="userdetails"]');
209         like( $driver->get_title, qr(Your library home), 'Patron with superlibrarian permission should be able to login to the OPAC');
210
211         $driver->find_element('//a[@id="user-menu"]')->click;
212         $driver->find_element('//a[@id="logout"]')->click;
213         $driver->find_element('//div[@id="login"]'); # logged out
214
215         subtest 'not authorized' => sub {
216             plan tests => 13;
217
218             $driver->get($mainpage . q|?logout.x=1|);
219             $driver->get($mainpage);
220             my $cookie = $driver->get_cookie_named('CGISESSID');
221             my $first_sessionID = $cookie->{value};
222
223             # User is not logged in, navigation does not generate a new cookie
224             $driver->get( $s->opac_base_url . q|opac-search.pl| );
225             like( $driver->get_title, qr(Advanced search) );
226             $cookie = $driver->get_cookie_named('CGISESSID');
227             is( $cookie->{value}, $first_sessionID, );
228
229             # Login
230             $driver->get($mainpage);
231             $s->fill_form( { userid => $patron->userid, password => $password } );
232             $s->submit_form;
233
234             # After logged in, the same cookie is reused
235             like( $driver->get_title, qr(Your library home) );
236             $cookie = $driver->get_cookie_named('CGISESSID');
237             is( $cookie->{value}, $first_sessionID, );
238             $driver->get( $s->opac_base_url . q|opac-search.pl| );
239             like( $driver->get_title, qr(Advanced search) );
240             $cookie = $driver->get_cookie_named('CGISESSID');
241             is( $cookie->{value}, $first_sessionID, );
242
243             # Logged in user can place holds
244             $driver->get( $s->opac_base_url . q|opac-reserve.pl| ); # We may need to pass a biblionumber here in the future
245             like( $driver->get_title, qr(Placing a hold) );
246             $cookie = $driver->get_cookie_named('CGISESSID');
247             is( $cookie->{value}, $first_sessionID, );
248
249             $driver->get($mainpage . q|?logout.x=1|);
250
251             # FIXME This new get should not be needed, but the cookie is not modified right after logout
252             # However it's not the behaviour when testing the UI
253             $driver->get($mainpage);
254
255             # After logout a new cookie is generated, the previous session has been deleted
256             $cookie = $driver->get_cookie_named('CGISESSID');
257             isnt( $cookie->{value}, $first_sessionID, );
258             $first_sessionID = $cookie->{value};
259
260             $driver->get( $s->opac_base_url . q|svc/checkout_notes| );
261             #FIXME - 500 is the current behaviour, but it's not nice. It could be improved.
262             like( $driver->get_title, qr(An error has occurred), 'Patron cannot access svc');
263             # No new cookie generated
264             $cookie = $driver->get_cookie_named('CGISESSID');
265             is( $cookie->{value}, $first_sessionID, );
266
267             $driver->get( $s->opac_base_url . q|opac-reserve.pl| );
268             like( $driver->get_title, qr(Log in to your account) );
269
270             # Still no new cookie generated
271             $driver->get($mainpage);
272             $cookie = $driver->get_cookie_named('CGISESSID');
273             is( $cookie->{value}, $first_sessionID, );
274         };
275
276         push @data_to_cleanup, $patron, $patron->category, $patron->library;
277     };
278
279     subtest 'Regressions' => sub {
280
281         plan tests => 2;
282
283         my $mainpage = $s->base_url . q|mainpage.pl|;
284
285         my $patron_1 = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 1 }});
286         my $patron_2 = $builder->build_object({ class => 'Koha::Patrons', value => { flags => 0 }});
287         my $password = 'password';
288         t::lib::Mocks::mock_preference( 'RequireStrongPassword', 0 );
289         $patron_1->set_password({ password => $password });
290         $patron_2->set_password({ password => $password });
291
292         $driver->get($mainpage . q|?logout.x=1|);
293         $s->auth( $patron_2->userid, $password );
294         like( $driver->get_title, qr(Access denied), 'Patron without permissions should not be able to login' );
295
296         $s->auth( $patron_1->userid, $password );
297         like( $driver->get_title(), qr(Koha staff interface), 'Patron with permissions should be able to login' );
298
299         push @data_to_cleanup, $patron_1, $patron_1->category, $patron_1->library;
300         push @data_to_cleanup, $patron_2, $patron_2->category, $patron_2->library;
301     };
302
303     $driver->quit();
304 };
305
306 END {
307     $_->delete for @data_to_cleanup;
308 };