File Coverage

blib/lib/Mail/Milter/Authentication/Tester/HandlerTester.pm
Criterion Covered Total %
statement 177 183 96.7
branch 37 50 74.0
condition 11 14 78.5
subroutine 28 31 90.3
pod 20 20 100.0
total 273 298 91.6


line stmt bran cond sub pod time code
1             package Mail::Milter::Authentication::Tester::HandlerTester;
2 12     12   74916 use 5.20.0;
  12         102  
3 12     12   69 use strict;
  12         28  
  12         238  
4 12     12   57 use warnings;
  12         25  
  12         296  
5 12     12   4096 use Mail::Milter::Authentication::Pragmas;
  12         53  
  12         104  
6             # ABSTRACT: Class for testing handlers
7             our $VERSION = '3.20230629'; # VERSION
8 12     12   10683 use Mail::Milter::Authentication;
  12         81  
  12         1216  
9 12     12   152 use Mail::Milter::Authentication::Protocol::Milter;
  12         48  
  12         386  
10 12     12   97 use Mail::Milter::Authentication::Protocol::SMTP;
  12         34  
  12         306  
11 12     12   6091 use Net::DNS::Resolver::Mock;
  12         75465  
  12         609  
12 12     12   131 use Net::IP;
  12         56  
  12         37452  
13              
14             sub _build_config_smtp {
15 14     14   66 my ( $self, $handler_config ) = @_;
16              
17 14         269 my $config = {
18              
19             '_is_test' => 1,
20             'debug' => 1,
21             'dryrun' => 0,
22             'logtoerr' => 1,
23             'error_log' => 'tmp/smtp.err',
24             'connection' => 'unix:tmp/authentication_milter_test.sock',
25             'umask' => '0000',
26             'connect_timeout' => 55,
27             'command_timeout' => 55,
28             'content_timeout' => 595,
29             'tempfail_on_error' => 1,
30             'tempfail_on_error_authenticated' => 1,
31             'tempfail_on_error_local' => 1,
32             'tempfail_on_error_trusted' => 1,
33              
34             '#metric_connection' => 'unix:tmp/authentication_milter_test_metrics.sock',
35             '#metric_umask' => '0000',
36              
37             'protocol' => 'smtp',
38             'smtp' => {
39             'sock_type' => 'unix',
40             'sock_path' => 'tmp/authentication_milter_smtp_out.sock',
41             'pipeline_limit' => '4',
42             },
43              
44             'handlers' => $handler_config,
45              
46             };
47              
48 14         122 return $config;
49             }
50              
51             sub _build_config_milter {
52 4     4   17 my ( $self, $handler_config ) = @_;
53              
54 4         70 my $config = {
55              
56             '_is_test' => 1,
57             'debug' => 1,
58             'dryrun' => 0,
59             'logtoerr' => 1,
60             'error_log' => 'tmp/milter.err',
61             'connection' => 'unix:tmp/authentication_milter_test.sock',
62             'umask' => '0000',
63             'connect_timeout' => 55,
64             'command_timeout' => 55,
65             'content_timeout' => 595,
66             'tempfail_on_error' => 1,
67             'tempfail_on_error_authenticated' => 1,
68             'tempfail_on_error_local' => 1,
69             'tempfail_on_error_trusted' => 1,
70              
71             '#metric_connection' => 'unix:tmp/authentication_milter_test_metrics.sock',
72             '#metric_umask' => '0000',
73              
74             'protocol' => 'milter',
75              
76             'handlers' => $handler_config,
77              
78             };
79              
80 4         38 return $config;
81             }
82              
83             sub new {
84 18     18 1 732092 my ( $class, $args ) = @_;
85 18         63 my $self = {};
86 18         70 bless $self, $class;
87              
88 18         147 $self->{ 'snapshots' } = {};
89              
90 18         85 foreach my $arg ( qw{ prefix zonefile zonedata } ) {
91 54 100       257 $self->{ $arg } = $args->{ $arg } if exists $args->{ $arg };
92             }
93              
94 18 50       155 croak 'prefix must be supplies' if ! exists $self->{ 'prefix' };
95 18 50 66     172 croak 'zonefile or zonedata cannot both be supplied' if ( exists $self->{ 'zonefile' } ) && ( exists $self->{ 'zonedata' });
96 18 50 66     175 $self->{ 'zonedata' } = q{} if ( ! exists $self->{ 'zonefile' } ) && ( ! exists $self->{ 'zonedata' });
97              
98 18   100     111 my $protocol = $args->{ 'protocol' } // 'smtp';
99              
100 18 50       104 if ( exists( $args->{ 'handler_config' } ) ) {
101 18 100       92 if ( $protocol eq 'smtp' ) {
102 14         110 set_config( $self->_build_config_smtp( $args->{ 'handler_config' } ) );
103             }
104             else {
105 4         25 set_config( $self->_build_config_milter( $args->{ 'handler_config' } ) );
106             }
107             }
108              
109 18         126 $Mail::Milter::Authentication::Config::PREFIX = $self->{ 'prefix' };
110 18         124 Mail::Milter::Authentication::Config::setup_config;
111              
112 18         100 my $config = get_config();
113              
114 18         370 my $Resolver = Net::DNS::Resolver::Mock->new();
115 18 100       9510 $Resolver->zonefile_read( $self->{ 'zonefile' } ) if exists $self->{ 'zonefile' };
116 18 100       147285 $Resolver->zonefile_parse( $self->{ 'zonedata' } ) if exists $self->{ 'zonedata' };
117 18         27192 $Mail::Milter::Authentication::Handler::TestResolver = $Resolver;
118              
119             # Setup a new authentication milter object
120 18         417 my $authmilter = Mail::Milter::Authentication->new();
121 18         414 $authmilter->{'metric'} = Mail::Milter::Authentication::Metric->new();
122 18         73 $authmilter->{'config'} = $config;
123              
124             # if ( $protocol eq 'smtp' ) {
125 18         389 push @Mail::Milter::Authentication::ISA, 'Mail::Milter::Authentication::Protocol::SMTP';
126             #}
127             #else {
128             #push @Mail::Milter::Authentication::ISA, 'Mail::Milter::Authentication::Protocol::Milter';
129             #}
130              
131             # Setup a fake server object
132 18         197 $authmilter->{ 'server' }->{ 'ppid' } = $PID;
133              
134             # Load handlers
135 18         54 foreach my $name ( @{$config->{'load_handlers'}} ) {
  18         70  
136 35         219 $authmilter->load_handler( $name );
137              
138 35         367 my $package = "Mail::Milter::Authentication::Handler::$name";
139 35         435 my $object = $package->new( $authmilter );
140 35 100       394 if ( $object->can( 'pre_loop_setup' ) ) {
141 8         45 $object->pre_loop_setup();
142             }
143 35 50       4642 if ( $object->can( 'register_metrics' ) ) {
144 35         220 $authmilter->{'metric'}->register_metrics( $object->register_metrics() );
145             }
146              
147             }
148              
149             # Init handlers
150              
151 18         74 my $callbacks_list = {};
152 18         121 my $callbacks = {};
153 18         47 my $handler = {};
154 18         46 my $object = {};
155 18         50 my $object_maker = {};
156 18         49 my $count = 0;
157              
158 18         58 $authmilter->{'callbacks_list'} = $callbacks_list;
159 18         52 $authmilter->{'callbacks'} = $callbacks;
160 18         223 $authmilter->{'count'} = $count;
161 18         50 $authmilter->{'handler'} = $handler;
162 18         72 $authmilter->{'object'} = $object;
163 18         48 $authmilter->{'object_maker'} = $object_maker;
164              
165 18         108 $authmilter->setup_handlers();
166              
167 18         115 $self->{ 'authmilter' } = $authmilter;
168              
169 18         102 $self->handler()->top_setup_callback();
170              
171 18         171 $self->snapshot( '_new' );
172              
173 18         207 return $self;
174             }
175              
176             sub snapshot {
177 25     25 1 142 my ( $self, $name ) = @_;
178 25         5097 my $snapshot = clone( $self->{ 'authmilter' } );
179 25         258 $self->{ 'snapshots' }->{ $name } = $snapshot;
180             }
181              
182             sub switch {
183 216     216 1 114109 my ( $self, $name ) = @_;
184 216 50       1212 croak 'unknown snapshot' if ! exists ( $self->{ 'snapshots' }->{ $name } );
185 216         46831 my $snapshot = clone( $self->{ 'snapshots' }->{ $name } );
186 216         2231 $self->{ 'authmilter' } = $snapshot;
187             }
188              
189             sub handler {
190 3244     3244 1 6255 my ( $self ) = @_;
191 3244         31647 return $self->{ 'authmilter' }->{ 'handler' }->{ '_Handler' };
192             }
193              
194             sub connect { ## no critic
195 185     185 1 765 my ( $self, $name, $ip ) = @_;
196 185         495 my $authmilter = $self->{ 'authmilter' };
197 185   50     505 my $ip_obj = eval{ Net::IP->new( $ip ) } // undef;
  185         1982  
198             # An undef here should not make it through to handlers, however
199             # for testing we will allow this.
200 185         211971 $self->handler()->remap_connect_callback( $name, $ip_obj );
201 185         533 return $self->handler()->top_connect_callback( $name, $self->handler()->{ 'ip_object' } );
202             }
203              
204             sub helo {
205 179     179 1 770 my ( $self, $helo ) = @_;
206 179         555 $self->handler()->remap_helo_callback( $helo );
207 179         624 return $self->handler()->top_helo_callback( $self->handler()->{ 'helo_name' } );
208             }
209              
210             sub mailfrom {
211 178     178 1 610 my ( $self, $from ) = @_;
212 178         543 return $self->handler()->top_envfrom_callback( $from );
213             }
214              
215             sub rcptto {
216 177     177 1 637 my ( $self, $to ) = @_;
217 177         561 return $self->handler()->top_envrcpt_callback( $to );
218             }
219              
220             sub header {
221 663     663 1 1942 my ( $self, $key, $value, $original ) = @_;
222 663 50       2810 $original = "$key: $value" if ! defined $original;
223 663         1867 return $self->handler()->top_header_callback( $key, $value, $original );
224             }
225              
226             sub end_of_headers {
227 175     175 1 524 my ( $self ) = @_;
228 175         560 return $self->handler()->top_eoh_callback();
229             }
230              
231             sub body {
232 175     175 1 583 my ( $self, $body ) = @_;
233 175         648 return $self->handler()->top_body_callback( $body );
234             }
235              
236             sub end_of_message {
237 175     175 1 640 my ( $self ) = @_;
238 175         686 return $self->handler()->top_eom_callback();
239             }
240              
241             sub close { ## no critic
242 12     12 1 47682 my ( $self ) = @_;
243 12         56 return $self->handler()->top_close_callback();
244             }
245              
246             sub abort {
247 0     0 1 0 my ( $self ) = @_;
248 0         0 return $self->handler()->top_abort_callback();
249             }
250              
251             sub addheader {
252 174     174 1 583 my ( $self ) = @_;
253 174         636 return $self->handler()->top_addheader_callback();
254             }
255              
256             sub run {
257 184     184 1 2661110 my ( $self, $args ) = @_;
258              
259 184         1105 $self->switch( '_new' );
260              
261 184         499 my $returncode;
262 184         1195 $returncode = $self->connect( $args->{ 'connect_name' }, $args->{ 'connect_ip' } );
263 184 100       927 die 'connect' if ( $returncode != SMFIS_CONTINUE );
264 178         1010 $returncode = $self->helo( $args->{ 'helo' } );
265 178 100       905 die 'helo' if ( $returncode != SMFIS_CONTINUE );
266 177         1040 $returncode = $self->mailfrom( $args->{ 'mailfrom' } );
267 177 100       970 die 'mailfrom' if ( $returncode != SMFIS_CONTINUE );
268 176         565 foreach my $rcptto ( @{ $args->{ 'rcptto' } } ) {
  176         896  
269 176         735 $returncode = $self->rcptto( $rcptto );
270 176 100       1140 die 'rcptto ' . $rcptto if ( $returncode != SMFIS_CONTINUE );
271             }
272              
273 175         783 my $body = $args->{ 'body' };
274 175         3038 $body =~ s/\r?\n/\n/g;
275              
276 175         1400 my @lines = split( /\n/, $body );
277              
278             # Process headers
279 175         619 my $buffer = q{};
280 175         896 while ( my $line = shift @lines ) {
281 1306         2552 chomp $line;
282 1306 50       2953 last if $line eq q{};
283              
284 1306 100       4232 if ( $line =~ /^\s/ ) {
285 644         2065 $buffer .= "\n" . $line;
286             }
287             else {
288 662 100       1786 if ( $buffer ) {
289 487         2605 my ( $key, $value ) = split( ':', $buffer, 2 );
290 487         1710 $key =~ s/\s+$//;
291 487         2334 $value =~ s/^\s+//;
292 487         1757 $returncode = $self->header( $key, $value );
293 487 100       1560 die "header $key: $value" if ( $returncode != SMFIS_CONTINUE );
294             }
295 661         2885 $buffer = $line;
296             }
297              
298             }
299 174 50       832 if ( $buffer ) {
300 174         1070 my ( $key, $value ) = split( ':', $buffer, 2 );
301 174         848 $key =~ s/\s+$//;
302 174         1062 $value =~ s/^\s+//;
303 174         582 $returncode = $self->header( $key, $value );
304 174 50       1144 die "header $key: $value" if ( $returncode != SMFIS_CONTINUE );
305             }
306              
307 174         852 $returncode = $self->end_of_headers();
308 174 50       945 die 'eoh' if ( $returncode != SMFIS_CONTINUE );
309              
310 174         1356 $returncode = $self->body( join( "\n", @lines) . "\n" );
311 174 50       1053 die 'body' if ( $returncode != SMFIS_CONTINUE );
312              
313 174         914 $returncode = $self->end_of_message();
314 174 50       837 die 'body' if ( $returncode != SMFIS_CONTINUE );
315              
316 174         923 $self->addheader();
317             # $self->close();
318             }
319              
320             sub get_return {
321 0     0 1 0 my ( $self ) = @_;
322 0         0 return $self->handler()->get_return();
323             }
324              
325             sub get_reject_mail {
326 0     0 1 0 my ( $self ) = @_;
327 0         0 return $self->handler()->get_reject_mail();
328             }
329              
330             sub servername {
331 202     202 1 3881 my ( $self ) = @_;
332 202         927 return 'handlertester.test.authmilter.org';
333             }
334              
335             sub get_authresults_header {
336 202     202 1 74964 my ( $self ) = @_;
337             # Build a Mail::AuthenticationReslts object
338 202   100     464 my $c_auth_headers = eval{ clone( $self->handler()->{ 'c_auth_headers'}->{'Authentication-Results'} ) } // [];
  202         743  
339 202   100     663 my $auth_headers = eval{ clone( $self->handler()->{ 'auth_headers'}->{'Authentication-Results'} ) } // [];
  202         627  
340 202         728 my @added_ar_headers = ( @{ $c_auth_headers }, @{ $auth_headers } );
  202         577  
  202         635  
341 202         1073 my $header = Mail::AuthenticationResults::Header->new()->set_value( Mail::AuthenticationResults::Header::AuthServID->new()->set_value( $self->servername() ) );
342 202         7463 foreach my $ar_header ( @added_ar_headers ) {
343 358         8029 eval{ $ar_header->orphan(); }; # Remove parent for testing.
  358         1258  
344 358         3697 $header->add_child( $ar_header );
345             }
346 202         9843 return $header;
347             }
348              
349             1;
350              
351             __END__
352              
353             =pod
354              
355             =encoding UTF-8
356              
357             =head1 NAME
358              
359             Mail::Milter::Authentication::Tester::HandlerTester - Class for testing handlers
360              
361             =head1 VERSION
362              
363             version 3.20230629
364              
365             =head1 SYNOPSIS
366              
367             Emulates an Authentication Milter environment with methods for testing Handlers.
368              
369             Can snapshot and restore state at any point.
370              
371             =head1 DESCRIPTION
372              
373             Make testing of Authentication Milter Handler modules easier.
374              
375             =head1 NAME
376              
377             Mail::Milter::Authentication::Tester::HandlerTester - Test harness for testing Authentication Milter Handlers
378              
379             =head1 CONSTRUCTOR
380              
381             =over
382              
383             =item new( $args )
384              
385             Instantiate a new HandlerTester object.
386              
387             $args is a hashref with the following entries.
388              
389             =over
390              
391             =item prefix
392              
393             Required
394              
395             The Prefix path containing the authentication milter config file(s). This should contain
396             all configuration files required for your test, the main authentication_milter.json file
397             can be overridden by the handler_config option (see below).
398              
399             This location should, for example, contain a valid mail-dmarc.ini for any tests using
400             the DMARC handler.
401              
402             =item handler_config
403              
404             If present, the config will be built from a generic default SMTP environment, with the given
405             HASHREF substituted as the Handler configuration. This eliminates the need to have a config file
406             for each handler configuration you wish to test.
407              
408             =item zonedata
409              
410             The zonefile data for use with Net::DNS::Resolver::Mock
411              
412             =item zonefile
413              
414             A zonefile for use with Net::DNS::Resolver::Mock
415              
416             =back
417              
418             =back
419              
420             =head1 METHODS
421              
422             =over
423              
424             =item snapshot( $name )
425              
426             Save a snapshot with the given name
427              
428             =item switch( $name )
429              
430             Restore state from the given snapshot
431              
432             =item handler()
433              
434             Returns the Handler object
435              
436             =item connect( $name, $ip )
437              
438             Call the connect callbacks with the given data.
439              
440             Returns the value of get_return()
441              
442             =item helo( $name )
443              
444             Call the helo callbacks with the given data.
445              
446             Returns the value of get_return();
447              
448             =item mailfrom( $email )
449              
450             Call the envfrom callbacks with the given data.
451              
452             Returns the value of get_return();
453              
454             =item rcptto( $email )
455              
456             Call the envrcpt callbacks with the given data.
457              
458             Returns the value of get_return();
459              
460             =item header( $key, $value )
461              
462             Call the header callbacks with the given data.
463              
464             Returns the value of get_return()
465              
466             =item end_of_headers()
467              
468             Call the end_of_headers callbacks.
469              
470             Returns the value of get_return()
471              
472             =item body( $body_chunk )
473              
474             Call the body callbacks with the given data.
475              
476             Returns the value of get_return()
477              
478             =item end_of_message()
479              
480             Call the eom callbacks.
481              
482             Returns the value of get_return()
483              
484             =item close()
485              
486             Call the close callbacks.
487              
488             Returns the value of get_return()
489              
490             =item abort()
491              
492             Call the abort callbacks.
493              
494             =item addheader()
495              
496             Call the addheader callbacks.
497              
498             =item run( $args )
499              
500             Run with a given set of data as defined in $args hashref.
501              
502             Dies if the mail would be rejected.
503              
504             Arguments of $args are.
505              
506             =over
507              
508             =item connect_name
509              
510             The name of the connecting server.
511              
512             =item connect_ip
513              
514             The ip address of the connecting server.
515              
516             =item helo
517              
518             The helo string.
519              
520             =item mailfrom
521              
522             The envelope MAILFROM address.
523              
524             =item rcptto
525              
526             Arrayref of the envelope RCPTTO addresses.
527              
528             =item body
529              
530             The email body.
531              
532             =back
533              
534             =item get_return()
535              
536             Returns the value of get_return() from the current handler object.
537              
538             =item get_reject_mail()
539              
540             Returns the value of get_reject_mail() from the current handler object.
541              
542             =item servername()
543              
544             Returns a dummy authservid servername.
545              
546             =item get_authresults_header()
547              
548             Returns a Mail::AuthenticationResults::Header object representing the authentication results
549             header which would be added to the message.
550              
551             =back
552              
553             =head1 DEPENDENCIES
554              
555             Carp
556             Clone
557             Mail::AuthenticationResults::Header
558             Mail::AuthenticationResults::Header::AuthServID
559             Mail::Milter::Authentication
560             Mail::Milter::Authentication::Protocol::Milter
561             Mail::Milter::Authentication::Protocol::SMTP
562             Mail::Milter::Authentication::Config
563             Module::Load
564             Net::DNS::Resolver::Mock
565              
566             =head1 AUTHORS
567              
568             Marc Bradshaw E<lt>marc@marcbradshaw.netE<gt>
569              
570             =head1 COPYRIGHT
571              
572             Copyright 2018
573              
574             This library is free software; you may redistribute it and/or
575             modify it under the same terms as Perl itself.
576              
577             =head1 AUTHOR
578              
579             Marc Bradshaw <marc@marcbradshaw.net>
580              
581             =head1 COPYRIGHT AND LICENSE
582              
583             This software is copyright (c) 2020 by Marc Bradshaw.
584              
585             This is free software; you can redistribute it and/or modify it under
586             the same terms as the Perl 5 programming language system itself.
587              
588             =cut