File Coverage

blib/lib/Test/Net/LDAP/Mock.pm
Criterion Covered Total %
statement 74 102 72.5
branch 28 32 87.5
condition 6 10 60.0
subroutine 14 26 53.8
pod 16 16 100.0
total 138 186 74.1


line stmt bran cond sub pod time code
1 13     13   61107 use 5.006;
  13         37  
  13         504  
2 13     13   58 use strict;
  13         15  
  13         364  
3 13     13   50 use warnings;
  13         17  
  13         500  
4              
5             package Test::Net::LDAP::Mock;
6              
7 13     13   62 use base 'Test::Net::LDAP';
  13         16  
  13         5248  
8              
9 13     13   67 use IO::Socket;
  13         24  
  13         107  
10 13     13   7995 use Net::LDAP;
  13         30  
  13         78  
11 13     13   704 use Net::LDAP::Constant qw(LDAP_SUCCESS);
  13         77  
  13         10223  
12              
13             =head1 NAME
14              
15             Test::Net::LDAP::Mock - A mock LDAP client with simulated search in memory
16              
17             =cut
18              
19             =head1 SYNOPSIS
20              
21             All the LDAP operations are performed in memory, instead of connecting to the
22             real LDAP server.
23              
24             use Test::Net::LDAP::Mock;
25             my $ldap = Test::Net::LDAP::Mock->new();
26              
27             C is a subclass of L, which is
28             a subclass of L.
29              
30             In the actual test code, L should be used to mock
31             all the C instances in your application code.
32              
33             use Test::More tests => 1;
34             use Test::Net::LDAP::Util qw(ldap_mockify);
35            
36             ldap_mockify {
37             # Anywhere in this block, all the occurrences of Net::LDAP::new are
38             # replaced by Test::Net::LDAP::Mock::new
39             ok my_application_routine();
40             };
41              
42             Note: if no LDAP entries have been added to the in-memory directory, the
43             C method will silently succeed with no entries found.
44              
45             Below is an example to set up some fake data for particular test cases.
46              
47             use Test::More tests => 1;
48             use Test::Net::LDAP::Util qw(ldap_mockify);
49            
50             ldap_mockify {
51             my $ldap = Net::LDAP->new('ldap.example.com');
52            
53             $ldap->add('uid=user1, ou=users, dc=example, dc=com');
54             $ldap->add('uid=user2, ou=users, dc=example, dc=com');
55             $ldap->add('cn=group1, ou=groups, dc=example, dc=com', attrs => [
56             member => [
57             'uid=user1, ou=users, dc=example, dc=com',
58             'uid=user2, ou=users, dc=example, dc=com',
59             ]
60             ]);
61            
62             ok my_application_routine();
63             };
64              
65             C maintains a shared LDAP directory tree for the same
66             host/port, while it separates the directory trees for different
67             host/port combinations.
68             Thus, it is important to specify a correct server location consistently.
69              
70             =head1 DESCRIPTION
71              
72             =head2 Overview
73              
74             C provides all the operations of C, while
75             they are performed in memory with fake data that are set up just for testing.
76              
77             It is most useful for developers who write testing for an application that
78             uses LDAP search, while they do not have full control over the organizational
79             LDAP server.
80             In many cases, developers do not have write access to the LDAP data, and the
81             organizational information changes over time, which makes it difficult to write
82             stable test cases with LDAP.
83             C helps developers set up any fake LDAP directory tree
84             in memory, so that they can test sufficient varieties of senarios for the
85             application.
86              
87             Without this module, an alternative way to test an application using LDAP is to
88             run a real server locally during testing. (See how C is tested with
89             a local OpenLDAP server.)
90             However, it may not be always trivial to set up such a server with correct
91             configurations and schemas, where this module makes testing easier.
92              
93             =head2 LDAP Schema
94              
95             In the current version, the LDAP schema is ignored when entries are added or
96             modified, although a schema can optionally be specified only for the search
97             filter matching (based on L).
98              
99             An advantage is that it is much easier to set up fake data with any arbitrary
100             LDAP attributes than to care about all the restrictions with the schema.
101             A disadvantage is that it cannot test schema-sensitive cases.
102              
103             =head2 Controls
104              
105             LDAPv3 controls are not supported (yet).
106             The C parameter given as an argument of a method will be ignored.
107              
108             =head1 METHODS
109              
110             =head2 new
111              
112             Creates a new object. It does not connect to the real LDAP server.
113             Each object is associated with a shared LDAP data tree in memory, depending on
114             the target (host/port/path) and scheme (ldap/ldaps/ldapi).
115              
116             Test::Net::LDAP::Mock->new();
117             Test::Net::LDAP::Mock->new('ldap.example.com', port => 3389);
118              
119             =cut
120              
121             my $mock_map = {};
122             my $mock_target;
123              
124             sub new {
125 30     30 1 172 my $class = shift;
126 30   33     141 $class = ref $class || $class;
127 30 100       98 $class = __PACKAGE__ if $class eq 'Net::LDAP'; # special case (ldap_mockify)
128 30         58 my $target = &_mock_target;
129            
130 30         195 my $self = bless {
131             mock_data => undef,
132             net_ldap_socket => IO::Socket->new(),
133             }, $class;
134            
135 30   66     2505 $self->{mock_data} = ($mock_map->{$target} ||= do {
136 20         2265 require Test::Net::LDAP::Mock::Data;
137 20         149 Test::Net::LDAP::Mock::Data->new($self);
138             });
139            
140 30         88 return $self;
141             }
142              
143             sub _mock_target {
144 30 100   30   102 my $host = shift if @_ % 2;
145 30         87 my $arg = &Net::LDAP::_options;
146              
147 30 100       443 if ($mock_target) {
148 12         12 my ($new_host, $new_arg);
149              
150 12 100       34 if (ref $mock_target eq 'CODE') {
    100          
    50          
151 4         10 ($new_host, $new_arg) = $mock_target->($host, $arg);
152             } elsif (ref $mock_target eq 'ARRAY') {
153 4         7 ($new_host, $new_arg) = @$mock_target;
154             } elsif (ref $mock_target eq 'HASH') {
155 0         0 $new_arg = $mock_target;
156             } else {
157 4         5 $new_host = $mock_target;
158             }
159              
160 12 50       58 $host = $new_host if defined $new_host;
161 12 100       42 $arg = {%$arg, %$new_arg} if defined $new_arg;
162             }
163              
164 30   100     135 my $scheme = $arg->{scheme} || 'ldap';
165              
166             # Net::LDAP->new() can take an array ref as hostnames, where
167             # the first host that we can connect to will be used.
168             # For the mock object, let's just pick the first one.
169 30 100       73 if (ref $host) {
170 1   50     4 $host = $host->[0] || '';
171             }
172            
173 30 100       76 if (length $host) {
174 20 100       58 if ($scheme ne 'ldapi') {
175 18 100       54 if ($arg->{port}) {
    50          
176 10         19 $host =~ s/:\d+$//;
177 10         24 $host .= ":$arg->{port}";
178             } elsif ($host !~ /:\d+$/) {
179 8         16 $host .= ":389";
180             }
181             }
182             } else {
183 10         69 $host = '';
184             }
185              
186 30         101 return "$scheme://$host";
187             }
188              
189             sub _mock_message {
190 170     170   163 my $self = shift;
191 170         452 my $mesg = $self->message(@_);
192 170         3784 $mesg->{resultCode} = LDAP_SUCCESS;
193 170         231 $mesg->{errorMessage} = '';
194 170         216 $mesg->{matchedDN} = '';
195 170         174 $mesg->{raw} = undef;
196 170         316 $mesg->{controls} = undef;
197 170         199 $mesg->{ctrl_hash} = undef;
198 170         370 return $mesg;
199             }
200              
201             #override
202             sub _send_mesg {
203 0     0   0 my $ldap = shift;
204 0         0 my $mesg = shift;
205 0         0 return $mesg;
206             }
207              
208             =head2 mock_data
209              
210             Retrieves the currently associated data tree (for the internal purpose only).
211              
212             =cut
213              
214             sub mock_data {
215 43     43 1 165 return shift->{mock_data};
216             }
217              
218             =head2 mock_schema
219              
220             Gets or sets the LDAP schema (L object) for the currently
221             associated data tree.
222              
223             In this version, the schema is used only for the search filter matching (based
224             on L internally).
225             It has no effect for any modification operations such as C, C, and
226             C.
227              
228             =cut
229              
230             sub mock_schema {
231 0     0 1 0 my $self = shift;
232 0         0 $self->mock_data->schema(@_);
233             }
234              
235             =head2 mock_root_dse
236              
237             Gets or sets the root DSE (L) for the currently associated
238             data tree.
239              
240             This should be set up as part of the test fixture before any successive call to
241             the C method, since L will cache the returned object.
242              
243             $ldap->mock_root_dse(
244             namingContexts => 'dc=example,dc=com'
245             );
246              
247             Note: the namingContexts value has no effect on the restriction with the
248             topmost DN. In other words, even if namingContexts is set to
249             'dc=example,dc=com', the C method still allows you to add an entry to
250             'dc=somewhere-else'.
251              
252             =cut
253              
254             sub mock_root_dse {
255 0     0 1 0 my $self = shift;
256 0         0 $self->mock_data->mock_root_dse(@_);
257             }
258              
259             =head2 mock_bind
260              
261             Gets or sets a LDAP result code (and optionally a message) that will be used as a message
262             returned by a later C call.
263              
264             use Net::LDAP::Constant qw(LDAP_INVALID_CREDENTIALS);
265             $ldap->mock_bind(LDAP_INVALID_CREDENTIALS);
266             $ldap->mock_bind(LDAP_INVALID_CREDENTIALS, 'Login failed');
267             # ...
268             my $mesg = $ldap->bind(...);
269             $mesg->code && die $mesg->error; #=> die('Login failed')
270              
271             In the list context, it returns an array of the code and message. In the scalar
272             context, it returns the code only.
273              
274             Alternatively, this method can take a callback subroutine:
275              
276             $ldap->mock_bind(sub {
277             my $arg = shift;
278             # Validate $arg->{dn} and $arg->{password}, etc.
279             if (...invalid credentials...) {
280             return LDAP_INVALID_CREDENTIALS;
281             }
282             });
283              
284             The callback can return a single value as the LDAP result code or an array in the form
285             C<($code, $message)>. If the callback returns nothing (or C), it is regarded as
286             C.
287              
288             =cut
289              
290             sub mock_bind {
291 0     0 1 0 my $self = shift;
292 0         0 $self->mock_data->mock_bind(@_);
293             }
294              
295             =head2 mock_password
296              
297             Gets or sets the password for the simple password authentication with C.
298              
299             $ldap->mock_password('uid=test, dc=example, dc=com' => 'test_password');
300             # Caution: Passwords should usually *not* be hard-coded like this. Consider to load
301             # passwords from a config file, etc.
302              
303             The passwords are stored with the entry node in the data tree.
304              
305             Once this method is used, the C call will check the credentials whenever the
306             C parameter is passed. Anonymous binding and all the other authentication
307             methods are not affected.
308              
309             =cut
310              
311             sub mock_password {
312 0     0 1 0 my $self = shift;
313 0         0 $self->mock_data->mock_password(@_);
314             }
315              
316             =head2 mock_target
317              
318             Gets or sets the target scheme://host:port to normalize the way for successive
319             C objects to resolve the associated data tree.
320              
321             It is useful when normalizing the target scheme://host:port for different
322             combinations. For example, if there are sub-domains (such as ldap1.example.com
323             and ldap2.example.com) that share the same data tree, the target host should be
324             normalized to be the single master server (such as ldap.example.com).
325              
326             Test::Net::LDAP::Mock->mock_target('ldap.example.com');
327             Test::Net::LDAP::Mock->mock_target('ldap.example.com', port => 3389);
328             Test::Net::LDAP::Mock->mock_target(['ldap.example.com', {port => 3389}]);
329             Test::Net::LDAP::Mock->mock_target({scheme => 'ldaps', port => 3389});
330              
331             Since this will affect all the successive calls to instantiate C,
332             it may not be ideal when your application uses connections to multiple LDAP
333             servers. In that case, you can specify a callback that will be invoked each
334             time a C object is instantiated.
335              
336             Test::Net::LDAP::Mock->mock_target(sub {
337             my ($host, $arg) = @_;
338             # Normalize $host, $arg->{port}, and $arg->{scheme}
339             $host = 'ldap.example1.com' if $host =~ /\.example1\.com$/;
340             $host = 'ldap.example2.com' if $host =~ /\.example2\.com$/;
341             return ($host, $arg);
342             });
343              
344             =cut
345              
346             sub mock_target {
347 6     6 1 677 my $class = shift;
348              
349 6 100       12 if (@_) {
350 3         4 my $old = $mock_target;
351 3         3 my $host = shift;
352              
353 3 100       12 if (@_ >= 2) {
    50          
354 1         4 $mock_target = [$host, {@_}];
355             } elsif (@_ == 1) {
356 0         0 my $arg = shift;
357 0         0 $mock_target = [$host, $arg];
358             } else {
359 2         3 $mock_target = $host;
360             }
361              
362 3         8 return $old;
363             } else {
364 3         16 return $mock_target;
365             }
366             }
367              
368             =head2 search
369              
370             Searches for entries in the currently associated data tree.
371              
372             $ldap->search(
373             base => 'dc=example, dc=com', scope => 'sub',
374             filter => '(cn=*)', attrs => ['uid', 'cn']
375             );
376              
377             See L for more parameter usage.
378              
379             =cut
380              
381             sub search {
382 11     11 1 31 my $ldap = shift;
383 11         30 return $ldap->mock_data->search(@_);
384             }
385              
386             =head2 compare
387              
388             Compares an attribute/value pair with an entry in the currently associated data
389             tree.
390              
391             $ldap->compare('uid=test, dc=example, dc=com',
392             attr => 'cn',
393             value => 'Test'
394             );
395              
396             See L for more parameter usage.
397              
398             =cut
399              
400             sub compare {
401 0     0 1 0 my $ldap = shift;
402 0         0 return $ldap->mock_data->compare(@_);
403             }
404              
405             =head2 add
406              
407             Adds an entry to the currently associated data tree.
408              
409             $ldap->add('uid=test, dc=example, dc=com', attrs => [
410             cn => 'Test'
411             ]);
412              
413             See L for more parameter usage.
414              
415             =cut
416              
417             sub add {
418 14     14 1 3389 my $ldap = shift;
419 14         44 return $ldap->mock_data->add(@_);
420             }
421              
422             =head2 modify
423              
424             Modifies an entry in the currently associated data tree.
425              
426             $ldap->modify('uid=test, dc=example, dc=com', add => [
427             cn => 'Test2'
428             ]);
429              
430             See L for more parameter usage.
431              
432             =cut
433              
434             sub modify {
435 0     0 1   my $ldap = shift;
436 0           return $ldap->mock_data->modify(@_);
437             }
438              
439             =head2 delete
440              
441             Deletes an entry from the currently associated data tree.
442              
443             $ldap->delete('uid=test, dc=example, dc=com');
444              
445             See L for more parameter usage.
446              
447             =cut
448              
449             sub delete {
450 0     0 1   my $ldap = shift;
451 0           return $ldap->mock_data->delete(@_);
452             }
453              
454             =head2 moddn
455              
456             Modifies DN of an entry in the currently associated data tree.
457              
458             $ldap->moddn('uid=test, dc=example, dc=com',
459             newrdn => 'uid=test2'
460             );
461              
462             See L for more parameter usage.
463              
464             =cut
465              
466             sub moddn {
467 0     0 1   my $ldap = shift;
468 0           return $ldap->mock_data->moddn(@_);
469             }
470              
471             =head2 bind
472              
473             Returns an expected result message if the bind result has previously been setup by the
474             C method. Otherwise, a success message is returned.
475              
476             =cut
477              
478             sub bind {
479 0     0 1   my $ldap = shift;
480 0           return $ldap->mock_data->bind(@_);
481             }
482              
483             =head2 unbind
484              
485             Returns a success message.
486              
487             =cut
488              
489             sub unbind {
490 0     0 1   my $ldap = shift;
491 0           return $ldap->mock_data->unbind(@_);
492             }
493              
494             =head2 abandon
495              
496             Returns a success message.
497              
498             =cut
499              
500             sub abandon {
501 0     0 1   my $ldap = shift;
502 0           return $ldap->mock_data->abandon(@_);
503             }
504              
505             1;