File Coverage

blib/lib/Test/DNS.pm
Criterion Covered Total %
statement 22 100 22.0
branch 0 34 0.0
condition 0 18 0.0
subroutine 8 21 38.1
pod 8 8 100.0
total 38 181 20.9


line stmt bran cond sub pod time code
1 7     7   146559 use strict;
  7         16  
  7         254  
2 7     7   50 use warnings;
  7         12  
  7         322  
3             package Test::DNS;
4             BEGIN {
5 7     7   111 $Test::DNS::VERSION = '0.13';
6             }
7             # ABSTRACT: Test DNS queries and zone configuration
8              
9 7     7   7088 use Moose;
  7         16306990  
  7         73  
10 7     7   66058 use Net::DNS;
  7         5715948  
  7         908  
11 7     7   10490 use Test::Deep 'cmp_bag';
  7         94287  
  7         873  
12 7     7   14064 use Set::Object 'set';
  7         63500  
  7         705  
13 7     7   58 use base 'Test::Builder::Module';
  7         14  
  7         10026  
14              
15             has 'nameservers' => (
16             is => 'rw',
17             isa => 'ArrayRef',
18             default => sub { [] },
19             trigger => sub {
20             my ( $self, $nameservers ) = @_;
21             $self->object->nameservers( @{$nameservers} );
22             },
23             );
24              
25             has 'object' => ( is => 'ro', isa => 'Net::DNS::Resolver', lazy_build => 1 );
26              
27             has 'follow_cname' => ( is => 'rw', isa => 'Bool', default => 0 );
28             has 'warnings' => ( is => 'rw', isa => 'Bool', default => 1 );
29              
30             my $CLASS = __PACKAGE__;
31              
32             sub BUILD {
33 0     0 1   $Test::Builder::Level += 1;
34 0           return;
35             }
36              
37             sub _build_object {
38 0     0     my $self = shift;
39              
40 0           return Net::DNS::Resolver->new(
41             nameservers => $self->nameservers,
42             );
43             }
44              
45             sub _handle_hash_format {
46 0     0     my ( $self, $type, $hashref, $test_name, $extra ) = @_;
47 0           my $EMPTY = q{};
48 0   0       $test_name ||= $EMPTY;
49              
50             # special hash construct
51 0 0 0       if ( @_ >= 3 || @_ <= 4 ) {
52             # $self, $type, $hashref OR
53             # $self, $type, $hashref, $test_name
54 0 0 0       if ( ref $hashref eq 'HASH' &&
      0        
55             ! ref $test_name &&
56             ref \$test_name eq 'SCALAR' ) {
57             # $hashref is hashref
58             # $test_name isn't a ref
59             # \$test_name is a SCALAR ref
60 0           while ( my ( $domain, $ips ) = each %{$hashref} ) {
  0            
61 0           $self->is_record( $type, $domain, $ips, $test_name );
62             }
63              
64 0           return 1;
65             }
66             }
67              
68 0           return;
69             }
70              
71             # A -> IP
72             sub is_a {
73 0     0 1   my ( $self, $domain, $ips, $test_name ) = @_;
74 0 0         $self->_handle_hash_format( 'A', $domain, $ips, $test_name ) ||
75             $self->is_record( 'A', $domain, $ips, $test_name );
76 0           return;
77             }
78              
79             # PTR -> A
80             sub is_ptr {
81 0     0 1   my ( $self, $ip, $domains, $test_name ) = @_;
82 0 0         $self->_handle_hash_format( 'PTR', $ip, $domains, $test_name ) ||
83             $self->is_record( 'PTR', $ip, $domains );
84 0           return;
85             }
86              
87             # Domain -> NS
88             sub is_ns {
89 0     0 1   my ( $self, $domain, $ns, $test_name ) = @_;
90 0 0         $self->_handle_hash_format( 'NS', $domain, $ns, $test_name ) ||
91             $self->is_record( 'NS', $domain, $ns );
92 0           return;
93             }
94              
95             # Domain -> MX
96             sub is_mx {
97 0     0 1   my ( $self, $domain, $mx, $test_name ) = @_;
98 0 0         $self->_handle_hash_format( 'MX', $domain, $mx, $test_name ) ||
99             $self->is_record( 'MX', $domain, $mx );
100 0           return;
101             }
102              
103             # Domain -> CNAME
104             sub is_cname {
105 0     0 1   my ( $self, $domain, $cname, $test_name ) = @_;
106 0 0         $self->_handle_hash_format( 'CNAME', $domain, $cname, $test_name ) ||
107             $self->is_record( 'CNAME', $domain, $cname );
108 0           return;
109             }
110              
111             # Domain -> TXT
112             sub is_txt {
113 0     0 1   my ( $self, $domain, $txt, $test_name ) = @_;
114 0 0         $self->_handle_hash_format( 'TXT', $domain, $txt, $test_name ) ||
115             $self->is_record( 'TXT', $domain, $txt );
116 0           return;
117             }
118              
119             sub _get_method {
120 0     0     my ( $self, $type ) = @_;
121 0           my %method_by_type = (
122             'A' => 'address',
123             'NS' => 'nsdname',
124             'MX' => 'exchange',
125             'PTR' => 'ptrdname',
126             'CNAME' => 'cname',
127             'TXT' => 'txtdata',
128             );
129              
130 0           my $method = $method_by_type{$type};
131 0 0         return $method ? $method : 0;
132             }
133              
134             sub _recurse_a_records {
135 0     0     my ( $self, $set, $rr ) = @_;
136 0           my $res = $self->object;
137              
138 0 0         if ( $rr->type eq 'CNAME' ) {
    0          
139 0           my $cname_method = $self->_get_method('CNAME');
140 0           my $cname = $rr->$cname_method;
141 0           my $query = $res->query( $cname, 'A' );
142              
143 0 0         if ($query) {
144 0           my @records = $query->answer;
145 0           foreach my $record (@records) {
146 0           $self->_recurse_a_records( $set, $record );
147             }
148             }
149             } elsif ( $rr->type eq 'A' ) {
150 0           my $a_method = $self->_get_method('A');
151 0           $set->insert( $rr->$a_method );
152             }
153              
154 0           return;
155             }
156              
157             sub is_record {
158 0     0 1   my ( $self, $type, $input, $expected, $test_name ) = @_;
159              
160 0           my $res = $self->object;
161 0           my $tb = $CLASS->builder;
162 0           my $method = $self->_get_method($type);
163 0           my $query_res = $res->query( $input, $type );
164 0           my $COMMASPACE = q{, };
165 0           my $results = set();
166              
167 0 0         ( ref $expected eq 'ARRAY' ) || ( $expected = [ $expected ] );
168 0   0       $test_name ||= "[$type] $input -> " . join $COMMASPACE, @{$expected};
  0            
169              
170 0 0         if (!$query_res) {
171 0           $self->_warn( $type, "'$input' has no query result" );
172 0           $tb->ok( 0, $test_name );
173 0           return;
174             }
175              
176 0           my @records = $query_res->answer;
177              
178 0           foreach my $rr (@records) {
179 0 0         if ( $rr->type ne $type ) {
180 0 0 0       if ( $rr->type eq 'CNAME' && $self->follow_cname ) {
181 0           $self->_recurse_a_records( $results, $rr );
182             } else {
183 0           $self->_warn( $type, 'got incorrect RR type: ' . $rr->type );
184             }
185             } else {
186 0           $results->insert( $rr->$method );
187             }
188             }
189              
190 0           cmp_bag( [ $results->members ], $expected, $test_name );
191              
192 0           return;
193             }
194              
195             sub _warn {
196 0     0     my ( $self, $type, $msg ) = @_;
197              
198 0 0         $self->warnings || return;
199              
200 0           chomp $msg;
201 0           my $tb = $CLASS->builder;
202 0           $tb->diag("!! Warning: [$type] $msg !!");
203              
204 0           return;
205             }
206              
207             1;
208              
209              
210              
211             =pod
212              
213             =head1 NAME
214              
215             Test::DNS - Test DNS queries and zone configuration
216              
217             =head1 VERSION
218              
219             version 0.13
220              
221             =head1 SYNOPSIS
222              
223             This module helps you write tests for DNS queries. You could test your domain
224             configuration in the world or on a specific DNS server, for example.
225              
226             use Test::DNS;
227             use Test::More tests => 4;
228              
229             my $dns = Test::DNS->new();
230              
231             $dns->is_ptr( '1.2.3.4' => 'single.ptr.record.com' );
232             $dns->is_ptr( '1.2.3.4' => [ 'one.ptr.record.com', 'two.ptr.record.com' ] );
233             $dns->is_ns( 'google.com' => [ map { "ns$_.google.com" } 1 .. 4 ] );
234             $dns->is_a( 'ns1.google.com' => '216.239.32.10' );
235              
236             ...
237              
238             =head1 DESCRIPTION
239              
240             Test::DNS allows you to run tests which translate as DNS queries. It's simple to
241             use and abstracts all the difficult query checks from you. It has a built-in
242             tests naming scheme so you don't really have to name your tests (as shown in all
243             the examples) even though it supports the option.
244              
245             use Test::DNS;
246             use Test::More tests => 1;
247              
248             my $dns = Test::DNS->new( nameservers => [ 'my.dns.server' ] );
249             $dns->is_ptr( '1.1.1.1' => 'my_new.mail.server' );
250              
251             That was a complete test script that will fetch the PTR (if there is one), warns
252             if it's missing one (an option you can remove via the I<warnings> attribute) and
253             checks against the domain you gave. You could also give for each test an
254             arrayref of expected values. That's useful if you want to check multiple values.
255             For example:
256              
257             use Test::DNS;
258             use Test::More tests => 1;
259              
260             my $dns = Test::DNS->new();
261             $dns->is_ns( 'my.domain' => [ 'ns1.my.domain', 'ns2.my.domain' ] );
262             # or
263             $dns->is_ns( 'my.domain' => [ map { "ns$_.my.domain" } 1 .. 5 ] );
264              
265             You can set the I<follow_cname> option if your PTR returns a CNAME instead of an
266             A record and you want to test the A record instead of the CNAME. This happened
267             to me at least twice and fumbled my tests. I was expecting an A record, but got
268             a CNAME to an A record. This is obviously legal DNS practices, so using the
269             I<follow_cname> attribute listed below, the test went with flying colors. This
270             is a recursive CNAME to A record function so you could handle multiple CNAME
271             chaining if one has such an odd case.
272              
273             New in version 0.04 is the option to give a hashref as the testing values (not
274             including a test name as well), which makes things much easier to test if you
275             want to run multiple tests and don't want to write multiple lines. This helps
276             connect Test::DNS with freshly-parsed data (YAML/JSON/XML/etc.).
277              
278             use Test::DNS;
279             use YAML 'LoadFile';
280             use Test::More tests => 2;
281              
282             my $dns = Test::DNS->new();
283             # running two DNS tests in one command!
284             $dns->is_ns( {
285             'first.domain' => [ map { "ns$_.first.domain" } 1 .. 4 ],
286             'second.domain' => [ map { "ns$_.second.domain" } 5, 6 ],
287             } );
288              
289             my $tests = LoadFile('tests.yaml');
290             $dns->is_a( $tests, delete $tests->{'name'} ); # $tests is a hashref
291              
292             =head1 EXPORT
293              
294             This module is completely Object Oriented, nothing is exported.
295              
296             =head1 ATTRIBUTES
297              
298             =head2 nameservers($arrayref)
299              
300             Same as in L<Net::DNS>. Sets the nameservers, accepts an arrayref.
301              
302             $dns->nameservers( [ 'IP1', 'DOMAIN' ] );
303              
304             =head2 warnings($boolean)
305              
306             Do you want to output warnings from the module (in valid TAP), such as when a
307             record doesn't a query result or incorrect types?
308              
309             This helps avoid common misconfigurations. You should probably keep it, but if
310             it bugs you, you can stop it using:
311              
312             $dns->warnings(0);
313              
314             Default: 1 (on).
315              
316             =head2 follow_cname($boolean)
317              
318             When fetching an A record of a domain, it may resolve to a CNAME instead of an A
319             record. That would result in a false-negative of sorts, in which you say "well,
320             yes, I meant the A record the CNAME record points to" but L<Test::DNS> doesn't
321             know that.
322              
323             If you want want Test::DNS to follow every CNAME recursively till it reaches the
324             actual A record and compare B<that> A record, use this option.
325              
326             $dns->follow_cname(1);
327              
328             Default: 0 (off).
329              
330             =head1 SUBROUTINES/METHODS
331              
332             =head2 is_a( $domain, $ips, [$test_name] )
333              
334             Check the A record resolving of domain or subdomain.
335              
336             $ip can be an arrayref.
337              
338             $test_name is not mandatory.
339              
340             $dns->is_a( 'domain' => 'IP' );
341              
342             $dns->is_a( 'domain', [ 'IP1', 'IP2' ] );
343              
344             =head2 is_ns( $domain, $ips, [$test_name] )
345              
346             Check the NS record resolving of a domain or subdomain.
347              
348             $ip can be an arrayref.
349              
350             $test_name is not mandatory.
351              
352             $dns->is_ns( 'domain' => 'IP' );
353              
354             $dns->is_ns( 'domain', [ 'IP1', 'IP2' ] );
355              
356             =head2 is_ptr( $ip, $domains, [$test_name] )
357              
358             Check the PTR records of an IP.
359              
360             $domains can be an arrayref.
361              
362             $test_name is not mandatory.
363              
364             $dns->is_ptr( 'IP' => 'ptr.records.domain' );
365              
366             $dns->is_ptr( 'IP', [ 'first.ptr.domain', 'second.ptr.domain' ] );
367              
368             =head2 is_mx( $domain, $domains, [$test_name] )
369              
370             Check the MX records of a domain.
371              
372             $domains can be an arrayref.
373              
374             $test_name is not mandatory.
375              
376             $dns->is_mx( 'domain' => 'mailer.domain' );
377              
378             $dns->is_ptr( 'domain', [ 'mailer1.domain', 'mailer2.domain' ] );
379              
380             =head2 is_cname( $domain, $domains, [$test_name] )
381              
382             Check the CNAME records of a domain.
383              
384             $domains can be an arrayref.
385              
386             $test_name is not mandatory.
387              
388             $dns->is_cname( 'domain' => 'sub.domain' );
389              
390             $dns->is_cname( 'domain', [ 'sub1.domain', 'sub2.domain' ] );
391              
392             =head2 is_txt( $domain, $txt, [$test_name] )
393              
394             Check the TXT records of a domain.
395              
396             $txt can be an arrayref.
397              
398             $test_name is not mandatory.
399              
400             $dns->is_txt( 'domain' => 'v=spf1 -all' );
401              
402             $dns->is_txt( 'domain', [ 'sub1.domain', 'sub2.domain' ] );
403              
404             =head2 is_record( $type, $input, $expected, [$test_name] )
405              
406             The general function all the other is_* functions run.
407              
408             $type is the record type (CNAME, A, NS, PTR, MX, etc.).
409              
410             $input is the domain or IP you're testing.
411              
412             $expected can be an arrayref.
413              
414             $test_name is not mandatory.
415              
416             $dns->is_record( 'CNAME', 'domain', 'sub.domain', 'test_name' );
417              
418             =head2 BUILD
419              
420             L<Moose> builder method. Do not call it or override it. :)
421              
422             =head1 HASH FORMAT
423              
424             The hash format option (since version 0.04) allows you to run the tests using a
425             single hashref with an optional parameter for the test_name. The count is no
426             longer 1 (as it is with single tests), but each key/value pair represents a test
427             case.
428              
429             # these are 2 tests
430             $dns->is_ns( {
431             'first.domain' => [ map { "ns$_.first.domain" } 1 .. 4 ],
432             'second.domain' => [ map { "ns$_.second.domain" } 5, 6 ],
433             } );
434              
435             # number of tests: keys %{$tests}, test name: $tests->{'name'}
436             $dns->is_a( $tests, delete $tests->{'name'} ); # $tests is a hashref
437              
438             =head1 DEPENDENCIES
439              
440             L<Moose>
441              
442             L<Net::DNS>
443              
444             L<Test::Deep>
445              
446             L<Set::Object>
447              
448             =head1 AUTHOR
449              
450             Sawyer X, C<< <xsawyerx at cpan.org> >>
451              
452             =head1 BUGS
453              
454             Please report any bugs or feature requests to C<bug-test-dns at rt.cpan.org>, or
455             through the web interface at
456             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-DNS>. I will be notified,
457             and then you'll automatically be notified of progress on your bug as I make
458             changes.
459              
460             =head1 SUPPORT
461              
462             You can find documentation for this module with the perldoc command.
463              
464             perldoc Test::DNS
465              
466             You can also look for information at:
467              
468             =over 4
469              
470             =item * Github
471              
472             L<http://github.com/xsawyerx/test-dns>
473              
474             =item * RT: CPAN's request tracker
475              
476             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Test-DNS>
477              
478             =item * AnnoCPAN: Annotated CPAN documentation
479              
480             L<http://annocpan.org/dist/Test-DNS>
481              
482             =item * CPAN Ratings
483              
484             L<http://cpanratings.perl.org/d/Test-DNS>
485              
486             =item * Search CPAN
487              
488             L<http://search.cpan.org/dist/Test-DNS/>
489              
490             =back
491              
492             =head1 ACKNOWLEDGEMENTS
493              
494             =head1 LICENSE AND COPYRIGHT
495              
496             Copyright 2010 Sawyer X.
497              
498             This program is free software; you can redistribute it and/or modify it
499             under the terms of either: the GNU General Public License as published
500             by the Free Software Foundation; or the Artistic License.
501              
502             See http://dev.perl.org/licenses/ for more information.
503              
504             =head1 AUTHOR
505              
506             Sawyer X <xsawyerx@cpan.org>
507              
508             =head1 COPYRIGHT AND LICENSE
509              
510             This software is copyright (c) 2011 by Sawyer X.
511              
512             This is free software; you can redistribute it and/or modify it under
513             the same terms as the Perl 5 programming language system itself.
514              
515             =cut
516              
517              
518             __END__
519