Bug 16907: Koha::Patrons - Move DelMember to ->delete
[koha-ffzg.git] / t / db_dependent / Auth_with_ldap.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 use Test::More tests => 4;
21 use Test::MockModule;
22 use Test::MockObject;
23 use t::lib::TestBuilder;
24 use Test::Warn;
25
26 use C4::Context;
27
28 use Koha::Patrons;
29
30 my $dbh = C4::Context->dbh;
31
32 # Start transaction
33 $dbh->{AutoCommit} = 0;
34 $dbh->{RaiseError} = 1;
35
36 my $builder = t::lib::TestBuilder->new();
37
38 # Variables controlling LDAP server config
39 my $update         = 0;
40 my $replicate      = 0;
41 my $auth_by_bind   = 1;
42 my $anonymous_bind = 1;
43
44 # Variables controlling LDAP behaviour
45 my $desired_authentication_result = 'success';
46 my $desired_connection_result     = 'error';
47 my $desired_bind_result           = 'error';
48 my $desired_compare_result        = 'error';
49 my $desired_search_result         = 'error';
50 my $desired_count_result          = 1;
51 my $non_anonymous_bind_result     = 'error';
52 my $ret;
53
54 # Mock the context module
55 my $context = Test::MockModule->new('C4::Context');
56 $context->mock( 'config', \&mockedC4Config );
57
58 # Mock the Net::LDAP module
59 my $net_ldap = Test::MockModule->new('Net::LDAP');
60
61 $net_ldap->mock(
62     'new',
63     sub {
64         if ( $desired_connection_result eq 'error' ) {
65
66             # We were asked to fail the LDAP conexion
67             return;
68         }
69         else {
70             # Return a mocked Net::LDAP object (Test::MockObject)
71             return mock_net_ldap();
72         }
73     }
74 );
75
76 my $categorycode = $builder->build( { source => 'Category' } )->{categorycode};
77 my $branchcode   = $builder->build( { source => 'Branch' } )->{branchcode};
78 my $attr_type    = $builder->build(
79     {
80         source => 'BorrowerAttributeType',
81         value  => {
82             category_code => $categorycode
83         }
84     }
85 );
86
87 my $borrower = $builder->build(
88     {
89         source => 'Borrower',
90         value  => {
91             userid       => 'hola',
92             branchcode   => $branchcode,
93             categorycode => $categorycode
94         }
95     }
96 );
97
98 $builder->build(
99     {
100         source => 'BorrowerAttribute',
101         value  => {
102             borrowernumber => $borrower->{borrowernumber},
103             code           => $attr_type->{code},
104             attribute      => 'FOO'
105         }
106     }
107 );
108
109 # C4::Auth_with_ldap needs several stuff set first ^^^
110 use_ok('C4::Auth_with_ldap');
111 can_ok(
112     'C4::Auth_with_ldap', qw/
113       checkpw_ldap
114       search_method /
115 );
116
117 subtest 'checkpw_ldap tests' => sub {
118
119     plan tests => 4;
120
121     ## Connection fail tests
122     $desired_connection_result = 'error';
123     warning_is {
124         $ret =
125           C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola', password => 'hey' );
126     }
127     'LDAP connexion failed',
128       'checkpw_ldap prints correct warning if LDAP conexion fails';
129     is( $ret, 0, 'checkpw_ldap returns 0 if LDAP conexion fails' );
130
131     ## Connection success tests
132     $desired_connection_result = 'success';
133
134     subtest 'auth_by_bind = 1 tests' => sub {
135
136         plan tests => 8;
137
138         $auth_by_bind = 1;
139
140         $desired_authentication_result = 'success';
141         $anonymous_bind                = 1;
142         $desired_bind_result           = 'error';
143         $desired_search_result         = 'error';
144         reload_ldap_module();
145
146         warning_like {
147             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
148                 password => 'hey' );
149         }
150         qr/Anonymous LDAP bind failed: LDAP error #1: error_name/,
151           'checkpw_ldap prints correct warning if LDAP anonymous bind fails';
152         is( $ret, 0, 'checkpw_ldap returns 0 if LDAP anonymous bind fails' );
153
154         $desired_authentication_result = 'success';
155         $anonymous_bind                = 1;
156         $desired_bind_result           = 'success';
157         $desired_search_result         = 'success';
158         $desired_count_result          = 1;
159         $non_anonymous_bind_result     = 'success';
160         $update                        = 1;
161         reload_ldap_module();
162
163         my $auth = Test::MockModule->new('C4::Auth_with_ldap');
164         $auth->mock(
165             'update_local',
166             sub {
167                 return $borrower->{cardnumber};
168             }
169         );
170
171         C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola', password => 'hey' );
172         ok(
173             @{
174                 C4::Members::Attributes::GetBorrowerAttributes(
175                     $borrower->{borrowernumber}
176                 )
177             },
178             'Extended attributes are not deleted'
179         );
180         $auth->unmock('update_local');
181
182         $update               = 0;
183         $desired_count_result = 0;    # user auth problem
184         Koha::Patrons->find( $borrower->{borrowernumber} )->delete;
185         reload_ldap_module();
186         is(
187             C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola', password => 'hey' ),
188             0,
189             'checkpw_ldap returns 0 if user lookup returns 0'
190         );
191
192         $non_anonymous_bind_result = 'error';
193         reload_ldap_module();
194
195         warning_like {
196             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
197                 password => 'hey' );
198         }
199         qr/LDAP bind failed as kohauser hola: LDAP error #1: error_name/,
200           'checkpw_ldap prints correct warning if LDAP bind fails';
201         is( $ret, -1,
202             'checkpw_ldap returns -1 LDAP bind fails for user (Bug 8148)' );
203
204         # regression tests for bug 12831
205         $desired_authentication_result = 'error';
206         $anonymous_bind                = 0;
207         $desired_bind_result           = 'error';
208         $desired_search_result         = 'success';
209         $desired_count_result          = 0;           # user auth problem
210         $non_anonymous_bind_result     = 'error';
211         reload_ldap_module();
212
213         warning_like {
214             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
215                 password => 'hey' );
216         }
217         qr/LDAP bind failed as kohauser hola: LDAP error #1: error_name/,
218           'checkpw_ldap prints correct warning if LDAP bind fails';
219         is( $ret, 0,
220             'checkpw_ldap returns 0 LDAP bind fails for user (Bug 12831)' );
221
222     };
223
224     subtest 'auth_by_bind = 0 tests' => sub {
225
226         plan tests => 8;
227
228         $auth_by_bind = 0;
229
230         # Anonymous bind
231         $anonymous_bind            = 1;
232         $desired_bind_result       = 'error';
233         $non_anonymous_bind_result = 'error';
234         reload_ldap_module();
235
236         warning_like {
237             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
238                 password => 'hey' );
239         }
240 qr/LDAP bind failed as ldapuser cn=Manager,dc=metavore,dc=com: LDAP error #1: error_name/,
241           'checkpw_ldap prints correct warning if LDAP bind fails';
242         is( $ret, 0, 'checkpw_ldap returns 0 if bind fails' );
243
244         $anonymous_bind            = 1;
245         $desired_bind_result       = 'success';
246         $non_anonymous_bind_result = 'success';
247         $desired_compare_result    = 'error';
248         reload_ldap_module();
249
250         warning_like {
251             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
252                 password => 'hey' );
253         }
254 qr/LDAP Auth rejected : invalid password for user 'hola'. LDAP error #1: error_name/,
255           'checkpw_ldap prints correct warning if LDAP bind fails';
256         is( $ret, -1, 'checkpw_ldap returns -1 if bind fails (Bug 8148)' );
257
258         # Non-anonymous bind
259         $anonymous_bind            = 0;
260         $desired_bind_result       = 'success';
261         $non_anonymous_bind_result = 'error';
262         $desired_compare_result    = 'dont care';
263         reload_ldap_module();
264
265         warning_like {
266             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
267                 password => 'hey' );
268         }
269 qr/LDAP bind failed as ldapuser cn=Manager,dc=metavore,dc=com: LDAP error #1: error_name/,
270           'checkpw_ldap prints correct warning if LDAP bind fails';
271         is( $ret, 0, 'checkpw_ldap returns 0 if bind fails' );
272
273         $anonymous_bind            = 0;
274         $desired_bind_result       = 'success';
275         $non_anonymous_bind_result = 'success';
276         $desired_compare_result    = 'error';
277         reload_ldap_module();
278
279         warning_like {
280             $ret = C4::Auth_with_ldap::checkpw_ldap( $dbh, 'hola',
281                 password => 'hey' );
282         }
283 qr/LDAP Auth rejected : invalid password for user 'hola'. LDAP error #1: error_name/,
284           'checkpw_ldap prints correct warning if LDAP bind fails';
285         is( $ret, -1, 'checkpw_ldap returns -1 if bind fails (Bug 8148)' );
286
287     };
288 };
289
290 subtest 'search_method tests' => sub {
291
292     plan tests => 5;
293
294     my $ldap = mock_net_ldap();
295
296     # Null params tests
297     is( C4::Auth_with_ldap::search_method( $ldap, undef ),
298         undef, 'search_method returns undef on undefined userid' );
299     is( C4::Auth_with_ldap::search_method( undef, 'undef' ),
300         undef, 'search_method returns undef on undefined ldap object' );
301
302     # search ->code and !->code
303     $desired_search_result = 'error';
304     reload_ldap_module();
305     my $eval_retval =
306       eval { $ret = C4::Auth_with_ldap::search_method( $ldap, 'undef' ); };
307     like(
308         $@,
309         qr/LDAP search failed to return object : 1/,
310 'search_method prints correct warning when db->search returns error code'
311     );
312
313     $desired_search_result = 'success';
314     $desired_count_result  = 2;
315     reload_ldap_module();
316     warning_like { $ret = C4::Auth_with_ldap::search_method( $ldap, '123' ) }
317     qr/^LDAP Auth rejected \: \(uid\=123\) gets 2 hits/,
318       'search_method prints correct warning when hits count is not 1';
319     is( $ret, 0, 'search_method returns 0 when hits count is not 1' );
320
321 };
322
323 # Function that mocks the call to C4::Context->config(param)
324 sub mockedC4Config {
325     my $class = shift;
326     my $param = shift;
327
328     if ( $param eq 'useshibboleth' ) {
329         return 0;
330     }
331     if ( $param eq 'ldapserver' ) {
332         my %ldap_mapping = (
333             firstname    => { is => 'givenname' },
334             surname      => { is => 'sn' },
335             address      => { is => 'postaladdress' },
336             city         => { is => 'l' },
337             zipcode      => { is => 'postalcode' },
338             branchcode   => { is => 'branch' },
339             userid       => { is => 'uid' },
340             password     => { is => 'userpassword' },
341             email        => { is => 'mail' },
342             categorycode => { is => 'employeetype' },
343             phone        => { is => 'telephonenumber' },
344         );
345
346         my %ldap_config = (
347             anonymous_bind => $anonymous_bind,
348             auth_by_bind   => $auth_by_bind,
349             base           => 'dc=metavore,dc=com',
350             hostname       => 'localhost',
351             mapping        => \%ldap_mapping,
352             pass           => 'metavore',
353             principal_name => '%s@my_domain.com',
354             replicate      => $replicate,
355             update         => $update,
356             user           => 'cn=Manager,dc=metavore,dc=com',
357         );
358         return \%ldap_config;
359     }
360     if ( $param =~ /(intranetdir|opachtdocs|intrahtdocs)/x ) {
361         return q{};
362     }
363     if ( ref $class eq 'HASH' ) {
364         return $class->{$param};
365     }
366     return;
367 }
368
369 # Function that mocks the call to Net::LDAP
370 sub mock_net_ldap {
371
372     my $mocked_ldap = Test::MockObject->new();
373
374     $mocked_ldap->mock(
375         'bind',
376         sub {
377
378             my @args = @_;
379             my $mocked_message;
380
381             if ( $#args > 1 ) {
382
383                 # Args passed => non-anonymous bind
384                 if ( $non_anonymous_bind_result eq 'error' ) {
385                     return mock_net_ldap_message( 1, 1, 'error_name',
386                         'error_text' );
387                 }
388                 else {
389                     return mock_net_ldap_message( 0, 0, q{}, q{} );
390                 }
391             }
392             else {
393                 $mocked_message = mock_net_ldap_message(
394                     ( $desired_bind_result eq 'error' ) ? 1 : 0,    # code
395                     ( $desired_bind_result eq 'error' ) ? 1 : 0,    # error
396                     ( $desired_bind_result eq 'error' )
397                     ? 'error_name'
398                     : 0,                                            # error_name
399                     ( $desired_bind_result eq 'error' )
400                     ? 'error_text'
401                     : 0                                             # error_text
402                 );
403             }
404
405             return $mocked_message;
406         }
407     );
408
409     $mocked_ldap->mock(
410         'compare',
411         sub {
412
413             my $mocked_message;
414
415             if ( $desired_compare_result eq 'error' ) {
416                 $mocked_message =
417                   mock_net_ldap_message( 1, 1, 'error_name', 'error_text' );
418             }
419             else {
420                 # we expect return code 6 for success
421                 $mocked_message = mock_net_ldap_message( 6, 0, q{}, q{} );
422             }
423
424             return $mocked_message;
425         }
426     );
427
428     $mocked_ldap->mock(
429         'search',
430         sub {
431
432             return mock_net_ldap_search(
433                 {
434                     count => ($desired_count_result)
435                     ? $desired_count_result
436                     : 1,    # default to 1
437                     code => ( $desired_search_result eq 'error' )
438                     ? 1
439                     : 0,    # 0 == success
440                     error => ( $desired_search_result eq 'error' ) ? 1
441                     : 0,
442                     error_text => ( $desired_search_result eq 'error' )
443                     ? 'error_text'
444                     : undef,
445                     error_name => ( $desired_search_result eq 'error' )
446                     ? 'error_name'
447                     : undef,
448                     shift_entry => mock_net_ldap_entry( 'sampledn', 1 )
449                 }
450             );
451
452         }
453     );
454
455     return $mocked_ldap;
456 }
457
458 sub mock_net_ldap_search {
459     my ($parameters) = @_;
460
461     my $count       = $parameters->{count};
462     my $code        = $parameters->{code};
463     my $error       = $parameters->{error};
464     my $error_text  = $parameters->{error_text};
465     my $error_name  = $parameters->{error_name};
466     my $shift_entry = $parameters->{shift_entry};
467
468     my $mocked_search = Test::MockObject->new();
469     $mocked_search->mock( 'count',       sub { return $count; } );
470     $mocked_search->mock( 'code',        sub { return $code; } );
471     $mocked_search->mock( 'error',       sub { return $error; } );
472     $mocked_search->mock( 'error_name',  sub { return $error_name; } );
473     $mocked_search->mock( 'error_text',  sub { return $error_text; } );
474     $mocked_search->mock( 'shift_entry', sub { return $shift_entry; } );
475
476     return $mocked_search;
477 }
478
479 sub mock_net_ldap_message {
480     my ( $code, $error, $error_name, $error_text ) = @_;
481
482     my $mocked_message = Test::MockObject->new();
483     $mocked_message->mock( 'code',       sub { $code } );
484     $mocked_message->mock( 'error',      sub { $error } );
485     $mocked_message->mock( 'error_name', sub { $error_name } );
486     $mocked_message->mock( 'error_text', sub { $error_text } );
487
488     return $mocked_message;
489 }
490
491 sub mock_net_ldap_entry {
492     my ( $dn, $exists ) = @_;
493
494     my $mocked_entry = Test::MockObject->new();
495     $mocked_entry->mock( 'dn',     sub { return $dn; } );
496     $mocked_entry->mock( 'exists', sub { return $exists } );
497
498     return $mocked_entry;
499 }
500
501 # TODO: Once we remove the global variables in C4::Auth_with_ldap
502 # we shouldn't need this...
503 # ... Horrible hack
504 sub reload_ldap_module {
505     delete $INC{'C4/Auth_with_ldap.pm'};
506     require C4::Auth_with_ldap;
507     C4::Auth_with_ldap->import;
508     return;
509 }
510
511 $dbh->rollback;
512
513 1;