Bug 19532: (follow-up) aria-hidden attr on OPAC, and more
[koha.git] / t / Auth_with_shibboleth.t
1 #!/usr/bin/perl
2
3 # This file is part of Koha.
4 #
5 # Koha is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # Koha is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18 use Modern::Perl;
19
20 $| = 1;
21 use Module::Load::Conditional qw/check_install/;
22 use Test::More;
23 use Test::MockModule;
24 use Test::Warn;
25 use File::Temp qw(tempdir);
26
27 use t::lib::Mocks::Logger;
28
29 use utf8;
30 use CGI qw(-utf8 );
31 use C4::Context;
32
33 BEGIN {
34     if ( check_install( module => 'Test::DBIx::Class' ) ) {
35         plan tests => 18;
36     }
37     else {
38         plan skip_all => "Need Test::DBIx::Class";
39     }
40 }
41
42 use Test::DBIx::Class {
43     schema_class => 'Koha::Schema',
44     connect_info => [ 'dbi:SQLite:dbname=:memory:', '', '' ]
45 };
46
47 # Mock Variables
48 my $matchpoint = 'userid';
49 my $autocreate = 0;
50 my $sync = 0;
51 my %mapping    = (
52     'userid'       => { 'is' => 'uid' },
53     'surname'      => { 'is' => 'sn' },
54     'dateexpiry'   => { 'is' => 'exp' },
55     'categorycode' => { 'is' => 'cat' },
56     'address'      => { 'is' => 'add' },
57     'city'         => { 'is' => 'city' },
58 );
59 $ENV{'uid'}  = "test1234";
60 $ENV{'sn'}   = undef;
61 $ENV{'exp'}  = undef;
62 $ENV{'cat'}  = undef;
63 $ENV{'add'}  = undef;
64 $ENV{'city'} = undef;
65
66 # Setup Mocks
67 ## Mock Context
68 my $context = Test::MockModule->new('C4::Context');
69
70 ### Mock ->config
71 $context->mock( 'config', \&mockedConfig );
72
73 ### Mock ->preference
74 my $OPACBaseURL = "testopac.com";
75 my $staffClientBaseURL = "teststaff.com";
76 $context->mock( 'preference', \&mockedPref );
77
78 ### Mock ->tz
79 $context->mock( 'timezone', sub { return 'local'; } );
80
81 ### Mock ->interface
82 my $interface = 'opac';
83 $context->mock( 'interface', \&mockedInterface );
84
85 ## Mock Database
86 my $database = Test::MockModule->new('Koha::Database');
87
88 ### Mock ->schema
89 $database->mock( 'schema', \&mockedSchema );
90
91 # Tests
92 ##############################################################
93
94 my $logger = t::lib::Mocks::Logger->new();
95
96 # Can module load
97 use C4::Auth_with_shibboleth qw( shib_ok login_shib_url get_login_shib checkpw_shib );
98 require_ok('C4::Auth_with_shibboleth');
99
100 # Subroutine tests
101 ## shib_ok
102 subtest "shib_ok tests" => sub {
103     plan tests => 5;
104     my $result;
105
106     # correct config, no debug
107     is( shib_ok(), '1', "good config" );
108
109     # bad config, no debug
110     $matchpoint = undef;
111     warnings_are { $result = shib_ok() }
112     [ { carped => 'shibboleth matchpoint not defined' }, ],
113       "undefined matchpoint = fatal config, warning given";
114     is( $result, '0', "bad config" );
115
116     $matchpoint = 'email';
117     warnings_are { $result = shib_ok() }
118     [ { carped => 'shibboleth matchpoint not mapped' }, ],
119       "unmapped matchpoint = fatal config, warning given";
120     is( $result, '0', "bad config" );
121
122     # add test for undefined shibboleth block
123     $logger->clear;
124     reset_config();
125 };
126
127 ## logout_shib
128 #my $query = CGI->new();
129 #is(logout_shib($query),"https://".$opac."/Shibboleth.sso/Logout?return="."https://".$opac,"logout_shib");
130
131 ## login_shib_url
132 subtest "login_shib_url tests" => sub {
133     plan tests => 2;
134
135     my $string = 'language=en-GB&param="heh❤"';
136     my $query_string = Encode::encode('UTF-8', $string);
137     my $query_string_uri_escaped = URI::Escape::uri_escape_utf8('?'.$string);
138
139     local $ENV{REQUEST_METHOD} = 'GET';
140     local $ENV{QUERY_STRING}   = $query_string;
141     local $ENV{SCRIPT_NAME}    = '/cgi-bin/koha/opac-user.pl';
142     my $query = CGI->new($query_string);
143     is(
144         login_shib_url($query),
145         'https://testopac.com'
146           . '/Shibboleth.sso/Login?target='
147           . 'https://testopac.com/cgi-bin/koha/opac-user.pl'
148           . $query_string_uri_escaped,
149         "login shib url"
150     );
151
152     my $post_params = 'user=bob&password=wideopen';
153     local $ENV{REQUEST_METHOD} = 'POST';
154     local $ENV{CONTENT_LENGTH} = length($post_params);
155
156     my $dir = tempdir( CLEANUP => 1 );
157     my $infile = "$dir/in.txt";
158     open my $fh_write, '>', $infile or die "Could not open '$infile' $!";
159     print $fh_write $post_params;
160     close $fh_write;
161
162     open my $fh_read, '<', $infile or die "Could not open '$infile' $!";
163
164     $query = CGI->new($fh_read);
165     is(
166         login_shib_url($query),
167         'https://testopac.com'
168           . '/Shibboleth.sso/Login?target='
169           . 'https://testopac.com/cgi-bin/koha/opac-user.pl',
170         "login shib url"
171     );
172
173     close $fh_read;
174 };
175
176 ## get_login_shib
177 subtest "get_login_shib tests" => sub {
178
179     plan tests => 3;
180
181     my $login;
182
183     $login = get_login_shib();
184
185     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
186            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
187            ->clear();
188
189     is( $login, "test1234", "good config, attribute value returned" );
190 };
191
192 ## checkpw_shib
193 subtest "checkpw_shib tests" => sub {
194
195     plan tests => 33;
196
197     my $shib_login;
198     my ( $retval, $retcard, $retuserid );
199
200     # Setup Mock Database Data
201     fixtures_ok [
202         'Borrower' => [
203             [qw/cardnumber userid surname address city email/],
204             [qw/testcardnumber test1234 renvoize myaddress johnston  /],
205             [qw/testcardnumber1 test12345 clamp1 myaddress quechee kid@clamp.io/],
206             [qw/testcardnumber2 test123456 clamp2 myaddress quechee kid@clamp.io/],
207         ],
208         'Category' => [ [qw/categorycode default_privacy/], [qw/S never/], ]
209       ],
210       'Installed some custom fixtures via the Populate fixture class';
211
212     # good user
213     $shib_login = "test1234";
214     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
215
216     is( $logger->count(), 2,          "Two debugging entries");
217     is( $retval,    "1",              "user authenticated" );
218     is( $retcard,   "testcardnumber", "expected cardnumber returned" );
219     is( $retuserid, "test1234",       "expected userid returned" );
220     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
221            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
222            ->clear();
223
224     # bad user
225     $shib_login = 'martin';
226     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
227     is( $retval, "0", "user not authenticated" );
228     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
229            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
230            ->clear();
231
232     # duplicated matchpoint
233     $matchpoint = 'email';
234     $mapping{'email'} = { is => 'email' };
235     $shib_login = 'kid@clamp.io';
236     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
237     is( $retval, "0", "user not authenticated if duplicated matchpoint" );
238     $logger->debug_is("koha borrower field to match: email",  "borrower match field debug info")
239            ->debug_is("shibboleth attribute to match: email", "shib match attribute debug info")
240            ->clear();
241
242     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
243     $logger->debug_is("koha borrower field to match: email",  "borrower match field debug info")
244            ->debug_is("shibboleth attribute to match: email", "shib match attribute debug info")
245            ->warn_is('There are several users with email of kid@clamp.io, matchpoints must be unique', "duplicated matchpoint warned with debug")
246            ->clear();
247
248     reset_config();
249
250     # autocreate user
251     $autocreate  = 1;
252     $shib_login  = 'test4321';
253     $ENV{'uid'}  = 'test4321';
254     $ENV{'sn'}   = "pika";
255     $ENV{'exp'}  = "2017";
256     $ENV{'cat'}  = "S";
257     $ENV{'add'}  = 'Address';
258     $ENV{'city'} = 'City';
259
260     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
261     is( $retval,    "1",        "user authenticated" );
262     is( $retuserid, "test4321", "expected userid returned" );
263     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
264            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
265            ->clear();
266
267     ok my $new_user = ResultSet('Borrower')
268       ->search( { 'userid' => 'test4321' }, { rows => 1 } ), "new user found";
269     is_fields [qw/surname dateexpiry address city/], $new_user->next,
270       [qw/pika 2017 Address City/],
271       'Found $new_users surname';
272     $autocreate = 0;
273
274     # sync user
275     $sync = 1;
276     $ENV{'city'} = 'AnotherCity';
277     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
278     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
279            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
280            ->clear();
281
282     ok my $sync_user = ResultSet('Borrower')
283       ->search( { 'userid' => 'test4321' }, { rows => 1 } ), "sync user found";
284
285     is_fields [qw/surname dateexpiry address city/], $sync_user->next,
286       [qw/pika 2017 Address AnotherCity/],
287       'Found $sync_user synced city';
288     $sync = 0;
289
290     # good user
291     $shib_login = "test1234";
292     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
293     is( $retval,    "1",              "user authenticated" );
294     is( $retcard,   "testcardnumber", "expected cardnumber returned" );
295     is( $retuserid, "test1234",       "expected userid returned" );
296     $logger->debug_is("koha borrower field to match: userid", "borrower match field debug info")
297            ->debug_is("shibboleth attribute to match: uid",   "shib match attribute debug info")
298            ->clear();
299
300     # bad user
301     $shib_login = "martin";
302     ( $retval, $retcard, $retuserid ) = checkpw_shib($shib_login);
303     is( $retval, "0", "user not authenticated" );
304     $logger->info_is("There are several users with userid of martin, matchpoints must be unique", "Duplicated matchpoint warned to info");
305 };
306
307 ## _get_uri - opac
308 $OPACBaseURL = "testopac.com";
309 is( C4::Auth_with_shibboleth::_get_uri(),
310     "https://testopac.com", "https opac uri returned" );
311
312 $logger->clear;
313
314 $OPACBaseURL = "http://testopac.com";
315 my $result = C4::Auth_with_shibboleth::_get_uri();
316 is( $result, "https://testopac.com", "https opac uri returned" );
317 $logger->warn_is("Shibboleth requires OPACBaseURL/staffClientBaseURL to use the https protocol!", "Improper protocol logged to warn")
318        ->clear();
319
320 $OPACBaseURL = "https://testopac.com";
321 is( C4::Auth_with_shibboleth::_get_uri(),
322     "https://testopac.com", "https opac uri returned" );
323
324 $logger->clear();
325
326 $OPACBaseURL = undef;
327 $result = C4::Auth_with_shibboleth::_get_uri();
328 is( $result, "https://", "https $interface uri returned" );
329
330 $logger->warn_is("Syspref staffClientBaseURL or OPACBaseURL not set!", "undefined OPACBaseURL - received expected warning")
331        ->clear();
332
333 ## _get_uri - intranet
334 $interface = 'intranet';
335 $staffClientBaseURL = "teststaff.com";
336 is( C4::Auth_with_shibboleth::_get_uri(),
337     "https://teststaff.com", "https $interface uri returned" );
338
339
340 $logger->clear;
341
342 $staffClientBaseURL = "http://teststaff.com";
343 $result = C4::Auth_with_shibboleth::_get_uri();
344 is( $result, "https://teststaff.com", "https $interface uri returned" );
345 $logger->warn_is("Shibboleth requires OPACBaseURL/staffClientBaseURL to use the https protocol!")
346        ->clear;
347
348 $staffClientBaseURL = "https://teststaff.com";
349 is( C4::Auth_with_shibboleth::_get_uri(),
350     "https://teststaff.com", "https $interface uri returned" );
351 is( $logger->count(), 0, 'No logging' );
352
353 $staffClientBaseURL = undef;
354 $result = C4::Auth_with_shibboleth::_get_uri();
355 is( $result, "https://", "https $interface uri returned" );
356 $logger->warn_is("Syspref staffClientBaseURL or OPACBaseURL not set!", "undefined staffClientBaseURL - received expected warning")
357        ->clear;
358
359 ## _get_shib_config
360 # Internal helper function, covered in tests above
361
362 sub mockedConfig {
363     my $param = shift;
364
365     my %shibboleth = (
366         'autocreate' => $autocreate,
367         'sync'       => $sync,
368         'matchpoint' => $matchpoint,
369         'mapping'    => \%mapping
370     );
371
372     return \%shibboleth;
373 }
374
375 sub mockedPref {
376     my $param = $_[1];
377     my $return;
378
379     if ( $param eq 'OPACBaseURL' ) {
380         $return = $OPACBaseURL;
381     }
382
383     if ( $param eq 'staffClientBaseURL' ) {
384         $return = $staffClientBaseURL;
385     }
386
387     return $return;
388 }
389
390 sub mockedInterface {
391     return $interface;
392 }
393
394 sub mockedSchema {
395     return Schema();
396 }
397
398 ## Convenience method to reset config
399 sub reset_config {
400     $matchpoint = 'userid';
401     $autocreate = 0;
402     $sync = 0;
403     %mapping    = (
404         'userid'       => { 'is' => 'uid' },
405         'surname'      => { 'is' => 'sn' },
406         'dateexpiry'   => { 'is' => 'exp' },
407         'categorycode' => { 'is' => 'cat' },
408         'address'      => { 'is' => 'add' },
409         'city'         => { 'is' => 'city' },
410     );
411     $ENV{'uid'}  = "test1234";
412     $ENV{'sn'}   = undef;
413     $ENV{'exp'}  = undef;
414     $ENV{'cat'}  = undef;
415     $ENV{'add'}  = undef;
416     $ENV{'city'} = undef;
417
418     return 1;
419 }
420