feat: add full Zonemaster stack with Docker and Spanish UI

- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI)
- Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend
- Dockerfile.gui: Astro static build served via nginx
- docker-compose.yml: backend (internal) + frontend (port 5353)
- nginx.conf: root redirects to /es/, /api/ proxied to backend
- zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:19:24 +02:00
commit 8d4eaa1489
1567 changed files with 204155 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
use 5.014002;
use strict;
use warnings FATAL => 'all';
use Test::More;
plan tests => 1;
BEGIN {
use_ok( 'Zonemaster::CLI' ) || print "Bail out!\n";
}
diag( "Testing Zonemaster::CLI $Zonemaster::CLI::VERSION, Perl $], $^X" );

View File

@@ -0,0 +1,37 @@
#!perl
use v5.14.2;
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use File::Basename qw( dirname );
chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" );
my $makebin = 'make';
sub make {
my @make_args = @_;
undef $ENV{MAKEFLAGS};
my $command = join( ' ', $makebin, '-s', @make_args );
my $output = `$command 2>&1`;
if ( $? == -1 ) {
BAIL_OUT( "failed to execute: $!" );
}
elsif ( $? & 127 ) {
BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' );
}
return $output, $? >> 8;
}
subtest "distcheck" => sub {
my ( $output, $status ) = make "distcheck";
is $status, 0, $makebin . ' distcheck exits with value 0';
is $output, "", $makebin . ' distcheck gives empty output';
};

View File

@@ -0,0 +1,64 @@
#!perl
use v5.14.2;
use strict;
use warnings;
use utf8;
use Test::More; # see done_testing()
use File::Basename qw( dirname );
chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" );
chdir 'share' or BAIL_OUT( "chdir: $!" );
my $makebin = 'make';
sub make {
my @make_args = @_;
undef $ENV{MAKEFLAGS};
my $command = join( ' ', $makebin, '--silent', '--no-print-directory', @make_args );
my $output = `$command`;
if ( $? == -1 ) {
BAIL_OUT( "failed to execute: $!" );
}
elsif ( $? & 127 ) {
BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' );
}
return $output, $? >> 8;
}
subtest "no fuzzy marks" => sub {
my ( $output, $status ) = make "show-fuzzy";
is $status, 0, $makebin . ' show-fuzzy exits with value 0';
is $output, "", $makebin . ' show-fuzzy gives empty output';
};
subtest "check po files" => sub {
my ( $output, $status ) = make "check-po";
is $status, 0, $makebin . ' check-po exits with value 0';
is $output, "", $makebin . ' check-po gives empty output';
};
subtest "tidy po files" => sub {
SKIP: {
my ( $output, $status );
$output = `git diff --numstat`;
skip 'git repo should be clean to run this test', 3 if $output ne '';
diag "Using msgcat version: " . `msgcat --version | head -n1`;
( $output, $status ) = make "tidy-po";
is $status, 0, $makebin . ' tidy-po exits with value 0';
is $output, "", $makebin . ' tidy-po gives empty output';
$output = `git diff --numstat`;
is $output, "", 'all files are tidied (if not run "make tidy-po")';
}
};
done_testing();

18
zonemaster-cli/t/pod.t Normal file
View File

@@ -0,0 +1,18 @@
#!perl -T
use 5.006;
use strict;
use warnings FATAL => 'all';
use Test::More;
# Ensure a recent version of Test::Pod
my $min_tp = 1.22;
eval "use Test::Pod $min_tp";
plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
my @poddirs;
push @poddirs, ( -e 'lib' ? 'lib' : 'blib' );
push @poddirs, 'script';
all_pod_files_ok( all_pod_files( @poddirs ) );
done_testing;

View File

@@ -0,0 +1,320 @@
#!perl
use 5.14.2;
use utf8;
use warnings;
use Test::More;
use Log::Any::Test;
use Log::Any qw( $log );
use Test::Differences;
use Test::Exception;
use Zonemaster::Engine;
use Zonemaster::Engine::Profile;
use Zonemaster::CLI::TestCaseSet;
require Test::NoWarnings;
lives_ok { # Make sure we get to print log messages in case of errors.
subtest 'parse_modifier_expr' => sub {
my @cases = (
{
name => 'empty',
expr => '',
expected => [],
},
{
name => 'absolute term',
expr => 'term',
expected => [ '', 'term' ],
},
{
name => 'absolute additive',
expr => 'term',
expected => [ '', 'term' ],
},
{
name => 'absolute subtractive',
expr => 'term',
expected => [ '', 'term' ],
},
{
name => 'absolute multiple modifiers',
expr => 'term1+term2',
expected => [ '', 'term1', '+', 'term2' ],
},
{
name => 'relative multiple modifiers',
expr => '-term1+term2',
expected => [ '-', 'term1', '+', 'term2' ],
},
);
for my $case ( @cases ) {
subtest $case->{name} => sub {
my @actual = Zonemaster::CLI::TestCaseSet->parse_modifier_expr( $case->{expr} );
eq_or_diff \@actual, $case->{expected};
};
}
};
subtest 'new' => sub {
my @cases = (
{
name => 'empty',
schema => {},
selection => [],
expect_ok => {
terms => ['all'],
methods => [],
},
},
{
name => 'multiple test modules and test cases',
schema => { 'alpha' => [ 'bravo', 'charlie' ], 'delta' => ['echo'] },
selection => [ 'bravo', 'echo' ],
expect_ok => {
terms => [
'all', 'alpha', 'alpha/bravo', 'alpha/charlie',
'bravo', 'charlie', 'delta', 'delta/echo',
'echo'
],
methods => [ 'bravo', 'echo' ],
},
},
{
name => 'mixed cases',
schema => { 'alpha' => [ 'BRAVO', 'charlie' ] },
selection => [ 'bravo', 'CHARLIE' ],
expect_ok => {
terms => [ 'all', 'alpha', 'alpha/bravo', 'alpha/charlie', 'bravo', 'charlie' ],
methods => [ 'bravo', 'charlie' ],
},
},
{
name => 'illegal test module name 1',
schema => { 'all' => [] },
selection => [],
expect_err => qr/must not be 'all'/i,
},
{
name => 'illegal test module name 2',
schema => { 'ALL' => [] },
selection => [],
expect_err => qr/must not be 'all'/i,
},
{
name => 'illegal test module name 3',
schema => { 'alpha/bravo' => [] },
selection => [],
expect_err => qr{contains forbidden character '/'}i,
},
{
name => 'illegal test case name 1',
schema => { 'alpha' => ['all'] },
selection => [],
expect_err => qr/must not be 'all'/i,
},
{
name => 'illegal test case name 2',
schema => { 'alpha' => ['ALL'] },
selection => [],
expect_err => qr/must not be 'all'/i,
},
{
name => 'illegal test case name 3',
schema => { 'alpha' => ['bravo/charlie'] },
selection => [],
expect_err => qr{contains forbidden character '/'}i,
},
{
name => 'duplicate term 1',
schema => { 'alpha' => ['alpha'] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'duplicate term 2',
schema => { 'alpha' => ['ALPHA'] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'duplicate term 3',
schema => { 'ALPHA' => ['alpha'] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'duplicate term 4',
schema => { 'alpha' => [], 'bravo' => ['alpha'] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'duplicate term 5',
schema => { 'alpha' => [ 'bravo', 'bravo' ] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'duplicate term 6',
schema => { 'alpha' => ['bravo'], 'charlie' => ['bravo'] },
selection => [],
expect_err => qr/same name/i,
},
{
name => 'unrecognized test case 1',
schema => { 'alpha' => [] },
selection => ['all'],
expect_err => qr/unrecognized/i,
},
{
name => 'unrecognized test case 2',
schema => { 'alpha' => [] },
selection => ['alpha'],
expect_err => qr/unrecognized/i,
},
);
for my $case ( @cases ) {
subtest $case->{name} => sub {
my $test_case_set;
local $@;
eval {
$test_case_set = Zonemaster::CLI::TestCaseSet->new( #
$case->{selection},
$case->{schema},
);
};
my $err = $@;
my $actual;
if ( !$err ) {
$actual = {
terms => [ sort keys %{ $test_case_set->{_terms} } ],
methods => [ $test_case_set->to_list ],
};
}
if ( defined $case->{expect_err} ) {
like $err, $case->{expect_err}, "error";
}
else {
is $err, "", "no error";
}
if ( defined $case->{expect_ok} ) {
eq_or_diff $actual, $case->{expect_ok}, "result";
}
else {
eq_or_diff $actual, undef, "no result";
}
}; ## end sub
} ## end for my $case ( @cases )
}; ## end 'new' => sub
subtest 'apply_modifier' => sub {
my @cases = (
{
name => 'empty',
schema => {},
selection => [],
modifiers => [],
expected => [],
},
{
name => 'no modifiers',
schema => { basic => [ 'basic01', 'basic02' ] },
selection => ['basic01'],
modifiers => [],
expected => ['basic01'],
},
{
name => 'add a new case',
schema => { basic => [ 'basic01', 'basic02' ] },
selection => ['basic01'],
modifiers => [ '+', 'basic02' ],
expected => [ 'basic01', 'basic02' ],
},
{
name => 'add the same case',
schema => { basic => [ 'basic01', 'basic02' ] },
selection => ['basic01'],
modifiers => [ '+', 'basic01' ],
expected => ['basic01'],
},
{
name => 'replace',
schema => { basic => [ 'basic01', 'basic02' ] },
selection => ['basic01'],
modifiers => [ '', 'basic02' ],
expected => ['basic02'],
},
{
name => 'module expansion',
schema => { basic => ['basic01'], extra => [ 'extra01', 'extra02' ] },
selection => ['basic01'],
modifiers => [ '', 'extra' ],
expected => [ 'extra01', 'extra02' ],
},
{
name => 'all',
schema => { basic => ['basic01'], extra => [ 'extra01', 'extra02' ] },
selection => ['basic01'],
modifiers => [ '', 'all' ],
expected => [ 'basic01', 'extra01', 'extra02' ],
},
{
name => 'multiple modifiers',
schema => { basic => ['basic01'], extra => [ 'extra01', 'extra02' ] },
selection => ['basic01'],
modifiers => [ '', 'all', '-', 'basic' ],
expected => [ 'extra01', 'extra02' ],
},
{
name => 'invalid operator',
schema => { basic => [ 'basic01', 'basic02' ] },
selection => ['basic01'],
modifiers => [ '*', 'basic02' ],
error => qr{unrecognized operator}i,
},
);
for my $case ( @cases ) {
subtest $case->{name} => sub {
my $test_case_set = Zonemaster::CLI::TestCaseSet->new( #
$case->{selection},
$case->{schema},
);
local $@ = '';
eval {
while ( @{ $case->{modifiers} } ) {
my $op = shift @{ $case->{modifiers} };
my $term = shift @{ $case->{modifiers} };
$test_case_set->apply_modifier( $op, $term );
}
};
my $error = $@;
if ( exists $case->{expected} ) {
is $error, '';
eq_or_diff [ $test_case_set->to_list ], $case->{expected};
}
else {
like $error, $case->{error};
}
};
} ## end for my $case ( @cases )
};
};
for my $msg ( @{ $log->msgs } ) {
my $text = sprintf( "%s: %s", $msg->{level}, $msg->{message} );
if ( $msg->{level} =~ /trace|debug|info|notice/ ) {
note $text;
}
else {
diag $text;
}
}
Test::NoWarnings::had_no_warnings();
done_testing;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
ns1.test 192.0.2.1 {"9kfShNTLVy4wDehBPdpCXg":null}

View File

@@ -0,0 +1,2 @@
. NS ns1.test
ns1.test A 192.0.2.1

View File

@@ -0,0 +1,26 @@
b.root-servers.net 2001:0500:0200:0000:0000:0000:0000:000b {}
b.root-servers.net 199.9.14.201 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"Q/iEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA==","timestamp":1731580714.30933,"answerfrom":"199.9.14.201","querytime":0}}}}
l.root-servers.net 2001:0500:009f:0000:0000:0000:0000:0042 {}
l.root-servers.net 199.7.83.42 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"72GEAAABAAEADQAOAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBbMAeAAACAAEAB+kAAAQBbcAewBwAAQABAAfpAAAExikABMB0AAEAAQAH6QAABKr3qgLAgwABAAEAB+kAAATAIQQMwJIAAQABAAfpAAAExwdbDcChAAEAAQAH6QAABMDL5grAsAABAAEAB+kAAATABQXxwL8AAQABAAfpAAAEwHAkBMDOAAEAAQAH6QAABMZhvjXA3QABAAEAB+kAAATAJJQRwOwAAQABAAfpAAAEwDqAHsD7AAEAAQAH6QAABMEADoHBCgABAAEAB+kAAATHB1MqwRkAAQABAAfpAAAEygwbIQAAKRAAAAAAAAAA","querytime":0,"answerfrom":"199.7.83.42","timestamp":1731580714.6482}}}}
h.root-servers.net 198.97.190.53 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":0,"timestamp":1731580714.53711,"answerfrom":"198.97.190.53","data":"jU+EAAABAAEADQAOAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBbMAeAAACAAEAB+kAAAQBbcAewBwAAQABAAfpAAAExikABMB0AAEAAQAH6QAABKr3qgLAgwABAAEAB+kAAATAIQQMwJIAAQABAAfpAAAExwdbDcChAAEAAQAH6QAABMDL5grAsAABAAEAB+kAAATABQXxwL8AAQABAAfpAAAEwHAkBMDOAAEAAQAH6QAABMZhvjXA3QABAAEAB+kAAATAJJQRwOwAAQABAAfpAAAEwDqAHsD7AAEAAQAH6QAABMEADoHBCgABAAEAB+kAAATHB1MqwRkAAQABAAfpAAAEygwbIQAAKQTQAAAAAAAA"}}}}
h.root-servers.net 2001:0500:0001:0000:0000:0000:0000:0053 {}
j.root-servers.net 2001:0503:0c27:0000:0000:0000:0002:0030 {}
j.root-servers.net 192.58.128.30 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":0,"answerfrom":"192.58.128.30","timestamp":1731580714.58295,"data":"R7KEAAABAAEADQAOAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBbMAeAAACAAEAB+kAAAQBbcAewBwAAQABAAfpAAAExikABMB0AAEAAQAH6QAABKr3qgLAgwABAAEAB+kAAATAIQQMwJIAAQABAAfpAAAExwdbDcChAAEAAQAH6QAABMDL5grAsAABAAEAB+kAAATABQXxwL8AAQABAAfpAAAEwHAkBMDOAAEAAQAH6QAABMZhvjXA3QABAAEAB+kAAATAJJQRwOwAAQABAAfpAAAEwDqAHsD7AAEAAQAH6QAABMEADoHBCgABAAEAB+kAAATHB1MqwRkAAQABAAfpAAAEygwbIQAAKQXAAAAAAAAA"}}}}
f.root-servers.net 192.5.5.241 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":0,"timestamp":1731580714.41004,"answerfrom":"192.5.5.241","data":"1CWEAAABAAEADQAbAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAABAFjwB4AAAIAAQAH6QAABAFlwB4AAAIAAQAH6QAABAFrwB4AAAIAAQAH6QAABAFswB4AAAIAAQAH6QAABAFkwB4AAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBbcAewGcAAQABAAfpAAAEwCEEDMBnABwAAQAH6QAAECABBQAAAgAAAAAAAAAAAAzAdgABAAEAB+kAAATAy+YKwHYAHAABAAfpAAAQIAEFAACoAAAAAAAAAAAADsCFAAEAAQAH6QAABMEADoHAhQAcAAEAB+kAABAgAQf9AAAAAAAAAAAAAAABwJQAAQABAAfpAAAExwdTKsCUABwAAQAH6QAAECABBQAAnwAAAAAAAAAAAELAowABAAEAB+kAAATHB1sNwKMAHAABAAfpAAAQIAEFAAAtAAAAAAAAAAAADcAcAAEAAQAH6QAABMYpAATAHAAcAAEAB+kAABAgAQUDuj4AAAAAAAAAAgAwwL8AAQABAAfpAAAEqveqAsC/ABwAAQAH6QAAECgBAbgAEAAAAAAAAAAAAAvAzgABAAEAB+kAAATAJJQRwM4AHAABAAfpAAAQIAEH/gAAAAAAAAAAAAAAU8DdAAEAAQAH6QAABMAFBfHA3QAcAAEAB+kAABAgAQUAAC8AAAAAAAAAAAAPwOwAAQABAAfpAAAExmG+NcDsABwAAQAH6QAAECABBQAAAQAAAAAAAAAAAFPA+wABAAEAB+kAAATAcCQEwPsAHAABAAfpAAAQIAEFAAASAAAAAAAAAAANDcEKAAEAAQAH6QAABMA6gB7BCgAcAAEAB+kAABAgAQUDDCcAAAAAAAAAAgAwwRkAAQABAAfpAAAEygwbIcEZABwAAQAH6QAAECABDcMAAAAAAAAAAAAAADUAACn//wAAAAAAAA=="}}}}
f.root-servers.net 2001:0500:002f:0000:0000:0000:0000:000f {}
k.root-servers.net 193.0.14.129 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"EJGEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA==","timestamp":1731580714.61398,"answerfrom":"193.0.14.129","querytime":0}}}}
k.root-servers.net 2001:07fd:0000:0000:0000:0000:0000:0001 {}
e.root-servers.net 192.203.230.10 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"mDyEAAABAAEADQAOAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBbMAeAAACAAEAB+kAAAQBbcAewBwAAQABAAfpAAAExikABMB0AAEAAQAH6QAABKr3qgLAgwABAAEAB+kAAATAIQQMwJIAAQABAAfpAAAExwdbDcChAAEAAQAH6QAABMDL5grAsAABAAEAB+kAAATABQXxwL8AAQABAAfpAAAEwHAkBMDOAAEAAQAH6QAABMZhvjXA3QABAAEAB+kAAATAJJQRwOwAAQABAAfpAAAEwDqAHsD7AAEAAQAH6QAABMEADoHBCgABAAEAB+kAAATHB1MqwRkAAQABAAfpAAAEygwbIQAAKRAAAAAAAAAA","querytime":0,"timestamp":1731580714.3888,"answerfrom":"192.203.230.10"}}}}
e.root-servers.net 2001:0500:00a8:0000:0000:0000:0000:000e {}
d.root-servers.net 199.7.91.13 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"199.7.91.13","timestamp":1731580714.37701,"querytime":0,"data":"ZOyEAAABAAEADQAOAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAAAsAcAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBbMAeAAACAAEAB+kAAAQBbcAewBwAAQABAAfpAAAExikABMB0AAEAAQAH6QAABKr3qgLAgwABAAEAB+kAAATAIQQMwJIAAQABAAfpAAAExwdbDcChAAEAAQAH6QAABMDL5grAsAABAAEAB+kAAATABQXxwL8AAQABAAfpAAAEwHAkBMDOAAEAAQAH6QAABMZhvjXA3QABAAEAB+kAAATAJJQRwOwAAQABAAfpAAAEwDqAHsD7AAEAAQAH6QAABMEADoHBCgABAAEAB+kAAATHB1MqwRkAAQABAAfpAAAEygwbIQAAKQWqAAAAAAAA"}}}}
d.root-servers.net 2001:0500:002d:0000:0000:0000:0000:000d {}
g.root-servers.net 2001:0500:0012:0000:0000:0000:0000:0d0d {}
g.root-servers.net 192.112.36.4 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":0,"timestamp":1731580714.42995,"answerfrom":"192.112.36.4","data":"8aWEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA=="}}}}
m.root-servers.net 2001:0dc3:0000:0000:0000:0000:0000:0035 {}
m.root-servers.net 202.12.27.33 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1731580714.65964,"answerfrom":"202.12.27.33","querytime":0,"data":"reuEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA=="}}}}
a.root-servers.net 198.41.0.4 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"198.41.0.4","timestamp":1731580714.27699,"querytime":0,"data":"9WaEAAABAAEADQALAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAAAIAAQAH6QAABAFswB4AAAIAAQAH6QAABAFqwB4AAAIAAQAH6QAABAFmwB4AAAIAAQAH6QAABAFowB4AAAIAAQAH6QAABAFkwB4AAAIAAQAH6QAABAFiwB4AAAIAAQAH6QAABAFrwB4AAAIAAQAH6QAABAFpwB4AAAIAAQAH6QAABAFtwB4AAAIAAQAH6QAABAFlwB4AAAIAAQAH6QAABAFnwB4AAAIAAQAH6QAABAFjwB4AAAIAAQAH6QAAAsAcwGcAAQABAAfpAAAExwdTKsBnABwAAQAH6QAAECABBQAAnwAAAAAAAAAAAELAdgABAAEAB+kAAATAOoAewHYAHAABAAfpAAAQIAEFAwwnAAAAAAAAAAIAMMCFAAEAAQAH6QAABMAFBfHAhQAcAAEAB+kAABAgAQUAAC8AAAAAAAAAAAAPwJQAAQABAAfpAAAExmG+NcCUABwAAQAH6QAAECABBQAAAQAAAAAAAAAAAFPAowABAAEAB+kAAATHB1sNwLIAAQABAAfpAAAEqveqAgAAKRAAAAAAAAAA"}}},"UzfnoYKACE71u6V/QQ17nw":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"UCOEAAABAA0AAAANAAACAAEAAAIAAQAH6QAAFAFsDHJvb3Qtc2VydmVycwNuZXQAAAACAAEAB+kAAAQBasAeAAACAAEAB+kAAAQBZsAeAAACAAEAB+kAAAQBaMAeAAACAAEAB+kAAAQBZMAeAAACAAEAB+kAAAQBYsAeAAACAAEAB+kAAAQBa8AeAAACAAEAB+kAAAQBacAeAAACAAEAB+kAAAQBbcAeAAACAAEAB+kAAAQBZcAeAAACAAEAB+kAAAQBZ8AeAAACAAEAB+kAAAQBY8AeAAACAAEAB+kAAAQBYcAewBwAAQABAAfpAAAExwdTKsAcABwAAQAH6QAAECABBQAAnwAAAAAAAAAAAELAOwABAAEAB+kAAATAOoAewDsAHAABAAfpAAAQIAEFAwwnAAAAAAAAAAIAMMBKAAEAAQAH6QAABMAFBfHASgAcAAEAB+kAABAgAQUAAC8AAAAAAAAAAAAPwFkAAQABAAfpAAAExmG+NcBZABwAAQAH6QAAECABBQAAAQAAAAAAAAAAAFPAaAABAAEAB+kAAATHB1sNwGgAHAABAAfpAAAQIAEFAAAtAAAAAAAAAAAADcB3AAEAAQAH6QAABKr3qgLAdwAcAAEAB+kAABAoAQG4ABAAAAAAAAAAAAALAAApEAAAAAAAAAA=","timestamp":1731580714.23954,"answerfrom":"198.41.0.4","querytime":0}}}}
a.root-servers.net 2001:0503:ba3e:0000:0000:0000:0002:0030 {}
i.root-servers.net 2001:07fe:0000:0000:0000:0000:0000:0053 {}
i.root-servers.net 192.36.148.17 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"tQyEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA==","querytime":0,"timestamp":1731580714.57324,"answerfrom":"192.36.148.17"}}}}
c.root-servers.net 192.33.4.12 {"9kfShNTLVy4wDehBPdpCXg":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"b2iEAAABAAEAAAABAAAGAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeKV9KAAABwgAAAOEAAk6gAABUYAAACkE0AAAAAAAAA==","querytime":0,"answerfrom":"192.33.4.12","timestamp":1731580714.34358}}}}
c.root-servers.net 2001:0500:0002:0000:0000:0000:0000:000c {}

View File

@@ -0,0 +1,20 @@
{
"net":{
"ipv4": false,
"ipv6": false
},
"resolver":{
"source4": "192.0.2.1",
"source6": "2001:db8::1"
},
"test_levels" : {
"BASIC" : {
"B01_CHILD_FOUND" : "NOTICE",
"B01_ROOT_HAS_NO_PARENT" : "WARNING",
"B02_AUTH_RESPONSE_SOA" : "ERROR"
},
"SYSTEM" : {
"GLOBAL_VERSION" : "INFO"
}
}
}

712
zonemaster-cli/t/usage.t Normal file
View File

@@ -0,0 +1,712 @@
#!perl
use 5.16.0;
use warnings;
use utf8;
use Test::More;
use Config '%Config';
use Encode qw( decode_utf8 );
use File::Basename qw( dirname );
use File::Slurp qw( read_file write_file );
use File::Spec::Functions qw( catfile );
use File::Temp qw( tempdir );
use IPC::Open3;
use JSON::XS;
use POSIX;
use Readonly;
use Symbol qw( gensym );
use Test::Differences;
use Zonemaster::CLI;
use JSON::Validator;
# Force locale C for these unit tests. They depend on the print outs not being
# translated. See zonemaster/zonemaster-cli/issues/438 and
# https://github.com/zonemaster/zonemaster-cli/issues/438#issuecomment-2996235684
$ENV{LC_ALL} = "C.UTF-8";
delete $ENV{LANG};
delete $ENV{LANGUAGE};
# CONSTANTS
Readonly::Array my @SIG_NAMES => do {
my @sig_names;
@sig_names[ split ' ', $Config{sig_num} ] = split ' ', $Config{sig_name};
@sig_names;
};
Readonly::Scalar my $PATH_WRAPPER => catfile( dirname( __FILE__ ), 'usage.wrapper.pl' );
Readonly::Scalar my $PATH_NORMAL_DATAFILE => catfile( dirname( __FILE__ ), 'usage.normal.data' );
Readonly::Scalar my $PATH_FAKE_DATA_DATAFILE => catfile( dirname( __FILE__ ), 'usage.fake-data.data' );
Readonly::Scalar my $PATH_FAKE_ROOT_DATAFILE => catfile( dirname( __FILE__ ), 'usage.fake-root.data' );
Readonly::Array my @PERL => do {
# Detect whether Devel::Cover is running
my $is_covering = !!( eval 'Devel::Cover::get_coverage()' );
note $is_covering ? 'Devel::Cover running' : 'Devel::Cover not covering';
( $^X, $is_covering ? ( '-MDevel::Cover=-silent,1' ) : () )
};
# MUTABLE GLOBAL VARIABLES
our $test_datafile;
# SETUP
if ( $ENV{ZONEMASTER_RECORD} ) {
write_file $PATH_NORMAL_DATAFILE, '';
write_file $PATH_FAKE_DATA_DATAFILE, '';
write_file $PATH_FAKE_ROOT_DATAFILE, '';
}
# HELPERS
sub json_schema {
my ( $schema ) = @_;
my $validator = JSON::Validator->new;
$validator->schema( $schema );
return $validator;
}
sub check_success {
my ( $name, $args, $predicate ) = @_;
subtest $name => sub {
my $result = _run_zonemaster_cli( $test_datafile, @$args );
my $stdout = delete $result->{stdout};
my $stderr = delete $result->{stderr};
my $exitstatus = delete $result->{exitstatus};
if ( $stderr ne '' ) {
note "stderr:\n$stderr" =~ s/\n/\n /gr;
}
if ( ref $predicate eq 'CODE' ) {
if ( $predicate->( $stdout ) ) {
pass 'expected stdout (sub)';
}
else {
fail 'expected stdout (sub)';
diag "actual stdout:\n$stdout" =~ s/\n/\n /gr;
}
}
elsif ( ref $predicate eq 'Regexp' ) {
like $stdout, $predicate, 'expected stdout (regex)';
}
elsif ( blessed $predicate && blessed $predicate eq 'JSON::Validator' ) {
my @items = parse_json_stream( $stdout );
my @errors = $predicate->validate( [@items] );
if ( !eq_or_diff \@errors, [], "schema validation" ) {
diag "actual stdout:\n$stdout" =~ s/\n/\n /gr;
}
}
else {
BAIL_OUT( "unrecognized predicate type" );
}
is $exitstatus, $Zonemaster::CLI::EXIT_SUCCESS, 'success exit status';
}; ## end sub
} ## end sub check_success
sub check_success_report {
my ( $name, $args, $predicates ) = @_;
subtest $name => sub {
check_success 'normal mode', $args, $predicates->{text};
check_success 'raw mode', $args, $predicates->{text};
check_success 'json mode', [ '--json', @$args ],
json_schema(
{
type => "array",
items => $predicates->{json},
}
);
check_success 'json-stream mode', [ '--json-stream', @$args ],
json_schema(
{
type => "array",
contains => $predicates->{json},
}
);
};
}
sub check_usage_error {
my ( $name, $args, $error_pattern ) = @_;
subtest $name => sub {
my $result = _run_zonemaster_cli( undef, @$args );
my $stderr = delete $result->{stderr};
like $stderr, $error_pattern, 'expected error message';
eq_or_diff(
$result,
{
stdout => '',
exitstatus => $Zonemaster::CLI::EXIT_USAGE_ERROR,
},
'no stdout and usage error exit code'
) or diag "stderr:\n$stderr" =~ s/\n/\n /gr;
};
}
sub parse_json_stream {
my ( $text ) = @_;
my $decoder = JSON::XS->new;
my @items;
while ( 1 ) {
$text =~ s/^\s+//;
if ( $text eq '' ) {
last;
}
my ( $item, $len ) = $decoder->decode_prefix( $text );
push @items, $item;
$text = substr $text, $len;
$text =~ s/^\s+//;
}
return @items;
} ## end sub parse_json_stream
sub has_locale {
my ( $locale ) = @_;
my $old_locale = setlocale( LC_CTYPE );
my $success = defined setlocale( LC_CTYPE, $locale );
setlocale( LC_CTYPE, $old_locale );
return $success;
}
sub _run_zonemaster_cli {
my ( $datafile, @args ) = @_;
my @cmd = ( @PERL, $PATH_WRAPPER );
if ( defined $datafile ) {
if ( $ENV{ZONEMASTER_RECORD} ) {
push @cmd, '--record';
}
push @cmd, $datafile;
}
push @cmd, '--', @args;
my $pid = open3( my $stdin, my $stdout, my $stderr = gensym, @cmd );
waitpid( $pid, 0 );
my $exitcode = $?;
if ( POSIX::WIFEXITED( $exitcode ) ) {
local $/ = undef;
return {
stdout => scalar <$stdout>,
stderr => scalar <$stderr>,
exitstatus => POSIX::WEXITSTATUS( $exitcode ),
};
}
elsif ( POSIX::WIFSIGNALED( $exitcode ) ) {
die "child process terminated by signal: " . $SIG_NAMES[ POSIX::WTERMSIG( $exitcode ) ];
}
elsif ( POSIX::WIFSTOPPED( $exitcode ) ) {
die "child process stopped by signal: " . $SIG_NAMES[ POSIX::WSTOPSIG( $exitcode ) ];
}
else {
die "unrecognized exit code $exitcode";
}
} ## end sub _run_zonemaster_cli
# TESTS
do {
local $test_datafile = $PATH_NORMAL_DATAFILE;
note "TESTS USING $test_datafile FOR RECORDED DATA:";
check_success 'normal table output', [ '--test=basic01', '--level=INFO', '.' ], qr{
^
Seconds \s+ Level \s+ Message \n
=+ \s =+ \s =+ \n
\s* \Q0.00\E \s+ INFO \s+ .* \s [v0-9.]+ \s .* \n
}msx;
check_success 'normal table output, no optional fields',
[ '--test=basic01', '--level=INFO', '--no-time', '--no-show-level', '.' ], qr{
^
Message \n
=+ \n
Using .* \n
}msx;
check_success 'normal table output, no optional fields, using underscore alias',
[ '--test=basic01', '--level=INFO', '--no-time', '--no-show_level', '.' ], qr{
^
Message \n
=+ \n
Using .* \n
}msx;
check_success 'normal table output, all fields',
[ '--test=basic01', '--level=INFO', '--show-module', '--show-testcase', '.' ], qr{
^
Seconds \s+ Level \s+ Module \s+ Testcase \s+ Message \n
=+ \s =+ \s =+ \s =+ \s =+ \n
\s* \Q0.00\E \s+ INFO \s+ System \s+ Unspecified \s+ Using .* \n
}msx;
check_success 'normal table output, all fields, using underscore aliases',
[ '--test=basic01', '--level=INFO', '--show_module', '--show_testcase', '.' ], qr{
^
Seconds \s+ Level \s+ Module \s+ Testcase \s+ Message \n
=+ \s =+ \s =+ \s =+ \s =+ \n
\s* \Q0.00\E \s+ INFO \s+ System \s+ Unspecified \s+ Using .* \n
}msx;
check_success '--encoding', [ '--test=basic01', '--json', '--encoding', 'foobar', '.' ], qr{
\Q{"results":[]}\E
}msx;
check_success '--json', [ '--test=basic01', '--json', '.' ], qr{
\Q{"results":[]}\E
}msx;
check_success '--json-stream', [ '--test=basic01', '--json-stream', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( $_[0] ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ /^Using / ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--json_stream', [ '--test=basic01', '--json_stream', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( $_[0] ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ /^Using / ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--raw', [ '--test=basic01', '--level=INFO', '--raw', '.' ], qr{
^
\s* \Q0.00\E \s+ INFO \s+ GLOBAL_VERSION \s+ version= [v0-9.]+ \n
}msx;
SKIP: {
skip 'sv_SE.UTF-8 locale is unavailable', 5
if !has_locale( 'sv_SE.UTF-8' );
check_success '--json-stream --no-raw',
[ '--test=basic01', '--json-stream', '--no-raw', '--locale=sv_SE.UTF-8', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( decode_utf8( $_[0] ) ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ qr{^Använder } ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--json-stream --json-translate',
[ '--test=basic01', '--json-stream', '--json-translate', '--locale=sv_SE.UTF-8', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( decode_utf8( $_[0] ) ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ qr{^Använder } ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--json-stream --no-raw --locale',
[ '--test=basic01', '--json-stream', '--no-raw', '--locale=sv_SE.UTF-8', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( decode_utf8( $_[0] ) ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ qr{^Använder } ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--json-stream --json-translate --locale',
[ '--test=basic01', '--json-stream', '--no-raw', '--locale=sv_SE.UTF-8', '--level=INFO', '.' ], sub {
my $found = 0;
for my $item ( parse_json_stream( decode_utf8( $_[0] ) ) ) {
if ( $item->{tag} eq 'GLOBAL_VERSION' ) {
if ( $item->{message} !~ qr{^Använder } ) {
return 0;
}
$found = 1;
}
}
return $found;
};
check_success '--locale', [ '--test=basic01', '--locale=sv_SE.UTF-8', '.' ], qr{
\QSer OK ut.\E
}msx;
} ## end SKIP:
check_success_report '--count', [ '--test=basic01', '--count', '.' ], {
text => qr{
\QLooks OK.\E
.*
Level \s+ \QNumber of log entries\E
.*
INFO \s+ \d+
.*
DEBUG \s+ \d+
.*
Level \s+ \QMessage tag\E \s+ \QCount\E
.*
INFO \s+ \w+ \s+ \d+
.*
}msx,
json => {
type => "object",
required => ["count"],
patternProperties => {
'^[A-Z]+[0-9]*$' => {
type => "integer",
},
},
},
};
check_success_report '--nstimes', [ '--test=basic01', '--nstimes', '.' ], {
text => qr{
\QLooks OK.\E
.*
\QName servers\E \s+ Max \s+ Min \s+ Avg \s+ Stddev \s+ Median \s+ Total \s+ Count
.*
\QChild zone\E
.*
\QParent zone\E
.*
Other
.*
\QGrand total\E \s+ \d+
}msx,
json => {
type => "object",
required => ["nstimes"],
properties => {
nstimes => {
type => "array",
items => {
type => "object",
properties => {
child => {
type => "array",
items => {
type => "object",
required => [qw( avg max median min ns stddev total count)],
},
},
parent => {
type => "array",
items => {
type => "object",
required => [qw( avg max median min ns stddev total count)],
},
},
other => {
type => "array",
items => {
type => "object",
required => [qw( avg max median min ns stddev total count)],
},
},
},
},
},
},
},
};
check_success_report '--elapsed', [ '--test=basic01', '--elapsed', '.' ], {
text => qr{
\QLooks OK.\E
.*
\QTotal test run time:\E
}msx,
json => {
type => "object",
required => ["elapsed"],
properties => {
elapsed => {
type => "number",
},
},
},
};
check_success '--level',
[ '--profile=t/usage.profile', '--ipv4', '--sourceaddr4', '', '--test=basic', '--raw', '--level=notice', '.' ],
sub {
my $stdout = $_[0];
return ( $stdout =~ qr{NOTICE .* WARNING .* ERROR}msx )
&& ( $stdout !~ qr{INFO}msx );
};
check_success '--stop-level=', [ '--profile=t/usage.profile', '--stop-level=', '.' ], qr{Looks OK}i;
check_success '--stop-level=warning',
[
'--profile=t/usage.profile', '--ipv4', '--sourceaddr4', '',
'--test=basic', '--raw', '--stop-level=warning', '.'
],
sub {
my $stdout = $_[0];
return ( $stdout =~ qr{NOTICE .* WARNING}msx )
&& ( $stdout !~ qr{ERROR}m );
};
check_success '--stop_level',
[
'--profile=t/usage.profile', '--ipv4', '--sourceaddr4', '',
'--test=basic', '--raw', '--stop_level=warning', '.'
],
sub {
my $stdout = $_[0];
return ( $stdout =~ qr{NOTICE .* WARNING}msx )
&& ( $stdout !~ qr{ERROR}m );
};
my $tempdir = tempdir( CLEANUP => 1 );
my $savefile = catfile( $tempdir, 'saved.data' );
check_success 'run command', [ "--save=$savefile", '--test=basic01', '.' ], sub {
my @saved_lines = read_file $savefile;
my @expected_lines = read_file $PATH_NORMAL_DATAFILE;
return scalar( @saved_lines ) == scalar( @expected_lines );
};
};
do {
local $test_datafile = $PATH_FAKE_DATA_DATAFILE;
note "TESTS USING $test_datafile FOR RECORDED DATA:";
SKIP: {
skip 'crashing test that has never worked on replay (FIXME)', 2
if not $ENV{ZONEMASTER_RECORD};
check_success '--ns', [ '--noipv6', '--raw', '--ns=ns1.a.example/9.9.9.9', 'a.se' ], qr{B02_NO_WORKING_NS};
check_success '--ds',
[
'--noipv6', '--raw', '--test=dnssec02',
'--ds=0,8,2,0000000000000000000000000000000000000000000000000000000000000000',
'zonemaster.net'
],
qr{DS02_NO_DNSKEY_FOR_DS};
}
};
do {
local $test_datafile = $PATH_FAKE_ROOT_DATAFILE;
note "TESTS USING $test_datafile FOR RECORDED DATA:";
check_success '--hints', [ '--noipv6', '--raw', '--hints=t/usage.hints', 'example.' ], qr{CANNOT_CONTINUE}i;
};
do {
local $test_datafile = undef;
note "TESTS USING NO NETWORK AND NO FILE FOR RECORDED DATA:";
check_usage_error 'no domain', [], qr{must give the name of a domain to test}i;
check_usage_error 'too many domains', [ 'example.com', 'example.net' ],
qr{only one domain can be given for testing}i;
check_usage_error 'invalid domain', ['!%~&'], qr{character not permitted}i;
check_usage_error 'unrecognized option', ['--foobar'], qr{unknown option}i;
check_usage_error '--test BAD_MODULE', [ '--test', '!%~&', 'example.' ], qr{unrecognized term '!%~&' in --test}i;
check_usage_error '--test UNKNOWN_MODULE/TESTCASE', [ '--test', 'foobar/foobar01', 'example.' ],
qr{unrecognized term 'foobar/foobar01' in --test}i;
check_usage_error '--test MODULE/UNKNOWN_TESTCASE', [ '--test', 'basic/foobar01', 'example.' ],
qr{unrecognized term 'basic/foobar01' in --test}i;
check_usage_error '--test MODULE//TESTCASE', [ '--test', 'basic//basic01', 'example.' ],
qr{unrecognized term 'basic//basic01' in --test}i;
check_usage_error '--ns BAD_NAME', [ '--ns', '!%~&', 'example.' ], qr{invalid name}i;
check_usage_error '--ns NAME//IP', [ '--ns', 'ns1.example//192.0.2.1', 'example.' ], qr{--ns}i;
check_usage_error '--ns NAME/BAD_IP', [ '--ns', 'ns1.example/foobar', 'example.' ], qr{invalid ip address}i;
check_usage_error '--sourceaddr4', [ '--sourceaddr4', 'foobar', 'example.' ], qr{invalid value}i;
check_usage_error '--sourceaddr6', [ '--sourceaddr6', 'foobar', 'example.' ], qr{invalid value}i;
check_usage_error '--level BAD_LEVEL', [ '--level', 'foobar', 'example.' ], qr{--level}i;
check_usage_error '--stop-level BAD_LEVEL', [ '--stop-level', 'foobar', 'example.' ],
qr{failed to recognize stop level}i;
check_usage_error '--json-stream and --no-json', [ '--json-stream', '--no-json', 'example.' ],
qr{cannot be used together}i;
check_usage_error 'Bad --hints (directory)', [ '--hints', '/', 'example.' ],
qr{error loading hints file}i;
check_usage_error 'Bad --hints (syntax)', [ '--hints', 't/usage.t', 'example.' ],
qr{error loading hints file}i;
check_success '--help', ['--help'], qr{
^Usage:$
.*
zonemaster-cli
.*
^Options:$
.*
--test
}msx;
check_success '-h', ['-h'], qr{
^Usage:$
.*
zonemaster-cli
.*
^Options:$
.*
--test
}msx;
check_success 'Single-character option bundling', ['-h?'], qr{
--test
}msx;
check_success '--version', ['--version'], qr{
^\QZonemaster-CLI version\E .*
^\QZonemaster-Engine version\E .*
^\QZonemaster-LDNS version\E .*
^\QNL NetLabs LDNS version\E .*
}msx;
check_success '--list-tests', ['--list-tests'], qr{
Basic
.*
basic01
}msx;
check_success '--list_tests', ['--list_tests'], qr{
Basic
.*
basic01
}msx;
SKIP: {
skip 'test that hang on FreeBSD (FIXME, see #388)', 2;
check_success '--dump-profile', ['--dump-profile'], qr{
"no_network"
}msx;
check_success '--dump_profile', ['--dump_profile'], qr{
"no_network"
}msx;
};
check_success 'override profile', [ '--dump-profile', '--profile=t/usage.profile' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
my $ipv4 = exists $profile->{net}{ipv4} ? ( $profile->{net}{ipv4} ? '1' : '0' ) : '<missing>';
my $ipv6 = exists $profile->{net}{ipv6} ? ( $profile->{net}{ipv6} ? '1' : '0' ) : '<missing>';
my $source4 = $profile->{resolver}{source4} // '<missing>';
my $source6 = $profile->{resolver}{source6} // '<missing>';
return
( $ipv4 eq '0' )
&& ( $ipv6 eq '0' )
&& ( $source4 eq '192.0.2.1' )
&& ( $source6 eq '2001:db8::1' );
};
check_success 'override net.ipv4', [ '--dump-profile', '--profile=t/usage.profile', '--ipv4' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
my $ipv4 = exists $profile->{net}{ipv4} ? ( $profile->{net}{ipv4} ? '1' : '0' ) : '<missing>';
return $ipv4 eq '1';
};
check_success 'override net.ipv6', [ '--dump-profile', '--profile=t/usage.profile', '--ipv6' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
my $ipv6 = exists $profile->{net}{ipv6} ? ( $profile->{net}{ipv6} ? '1' : '0' ) : '<missing>';
return $ipv6 eq '1';
};
check_success 'override resolver.source4',
[ '--dump-profile', '--profile=t/usage.profile', '--sourceaddr4', '192.0.2.2' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
my $source4 = $profile->{resolver}{source4} // '<missing>';
return $source4 eq '192.0.2.2';
};
check_success 'override resolver.source6',
[ '--dump-profile', '--profile=t/usage.profile', '--sourceaddr6', '2001:db8::2' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
my $source6 = $profile->{resolver}{source6} // '<missing>';
return $source6 eq '2001:db8::2';
};
check_success 'override test_cases',
[ '--dump-profile', '--profile=t/usage.profile', '--test=basic01' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
return
ref $profile->{test_cases} eq 'ARRAY'
&& scalar @{ $profile->{test_cases} } == 1
&& $profile->{test_cases}[0] eq 'basic01';
};
check_success 'override test_cases twice',
[ '--dump-profile', '--profile=t/usage.profile', '--test=-all', '--test=+basic01' ], sub {
my ( $profile ) = parse_json_stream( $_[0] );
return
ref $profile->{test_cases} eq 'ARRAY'
&& scalar @{ $profile->{test_cases} } == 1
&& $profile->{test_cases}[0] eq 'basic01';
};
check_success '--restore', [ "--restore=$PATH_NORMAL_DATAFILE", '--test=basic01', '--level=INFO', '--raw', '.' ],
qr{B01_CHILD_FOUND};
};
done_testing;

View File

@@ -0,0 +1,85 @@
#!/usr/bin/env perl
use v5.16;
use warnings;
use File::Basename qw( dirname );
use File::Spec::Functions qw( catfile );
use Zonemaster::CLI;
use Zonemaster::Engine::Nameserver;
use Zonemaster::Engine::Profile;
use lib catfile( dirname( dirname( __FILE__ ) ), 'script' );
# Help Zonemaster::CLI find zonemaster-cli in test context
$Zonemaster::CLI::SCRIPT = catfile( dirname( dirname( __FILE__ ) ), 'script', 'zonemaster-cli' );
# Parse command line options upto and including '--'.
my $opt_record = 0;
my $opt_datafile;
while ( @ARGV ) {
my $arg = shift @ARGV;
if ( substr( $arg, 0, 2 ) eq '--' ) {
if ( $arg eq '--' ) {
last;
}
elsif ( $arg eq '--record' ) {
$opt_record = 1;
}
else {
die "unrecognized option '$arg'";
}
}
else {
if ( defined $opt_datafile ) {
die "too many data files provided";
}
$opt_datafile = $arg;
}
} ## end while ( @ARGV )
if ( $opt_record && !defined $opt_datafile ) {
die "must not specify --record without also specifying a data file";
}
# Prime Zonemaster Engine before letting zonemaster-cli do its thing
if ( !$opt_record ) {
Zonemaster::Engine::Profile->effective->set( q{no_network}, 1 );
}
if ( $opt_datafile ) {
Zonemaster::Engine::Nameserver->restore( $opt_datafile );
}
our $EXIT_STATUS;
our $EMITTED_WARNING = 0;
do {
# Intercept warn()
local $SIG{__WARN__} = sub {
print STDERR "__WARN__: " . $_[0];
$EMITTED_WARNING = 1;
};
# Run Zonemaster::CLI
eval {
$EXIT_STATUS = Zonemaster::CLI->run( @ARGV );
1;
} or do {
print STDERR $@;
$EXIT_STATUS = $Zonemaster::CLI::EXIT_GENERIC_ERROR;
};
};
# Wrap up and terminate
if ( $opt_record ) {
Zonemaster::Engine::Nameserver->save( $opt_datafile );
}
if ( $EMITTED_WARNING ) {
say STDERR "EXIT 125: one or more warnings were emitted";
exit 125;
}
exit $EXIT_STATUS;