Files

842 lines
41 KiB
Perl
Raw Permalink Normal View History

use 5.006;
use strict;
use warnings FATAL => 'all';
use Test::More;
use Log::Any::Test; # Must come before use Log::Any
use JSON::PP;
use Readonly;
use Test::Differences;
use Test::Exception;
use Log::Any qw( $log );
use Zonemaster::Engine::Profile;
# YAML representation of an example profile with all properties set
Readonly my $EXAMPLE_PROFILE_1_YAML => q(
---
resolver:
defaults:
fallback: true
igntc: false
recurse: true
retrans: 234
retry: 123
usevc: true
source4: 192.0.2.53
source6: 2001:db8::42
net:
ipv4: true
ipv6: false
no_network: true
cache:
redis:
server: 127.0.0.1:6379
expire: 3600
asn_db:
style: cymru
sources:
cymru:
- asn1.example.com
- asn2.example.com
logfilter:
Zone:
TAG:
- set: WARNING
when:
bananas: 0
test_levels:
Zone:
TAG: INFO
test_cases:
- Zone01
);
# JSON representation of an example profile with all properties set
Readonly my $EXAMPLE_PROFILE_1 => q(
{
"resolver": {
"defaults": {
"usevc": true,
"recurse": true,
"igntc": false,
"fallback": true,
"retry": 123,
"retrans": 234
},
"source4": "192.0.2.53",
"source6": "2001:db8::42"
},
"net": {
"ipv4": true,
"ipv6": false
},
"no_network": true,
"cache": {
"redis": {
"server": "127.0.0.1:6379",
"expire": 3600
}
},
"asn_db" : {
"style" : "cymru",
"sources" : {
"cymru" : [ "asn1.example.com", "asn2.example.com" ]
}
},
"logfilter": {
"Zone": {
"TAG": [
{
"when": {
"bananas": 0
},
"set": "WARNING"
}
]
}
},
"test_levels": {
"Zone": {
"TAG": "INFO"
}
},
"test_cases": [
"Zone01"
]
}
);
# JSON representation of an example profile with all properties set to values
# that are different from those in $EXAMPLE_PROFILE_1
Readonly my $EXAMPLE_PROFILE_2 => q(
{
"resolver": {
"defaults": {
"usevc": false,
"recurse": false,
"igntc": true,
"fallback": false,
"retry": 99,
"retrans": 88
},
"source4": "198.51.100.53",
"source6": "2001:db8::cafe"
},
"net": {
"ipv4": false,
"ipv6": true
},
"no_network": false,
"cache": {
"redis": {
"server": "127.0.0.2:6379",
"expire": 7200
}
},
"asn_db" : {
"style" : "ripe",
"sources" : {
"ripe" : [ "asn3.example.com", "asn4.example.com" ]
}
},
"logfilter": {
"Nameserver": {
"OTHER_TAG": [
{
"when": {
"apples": 1
},
"set": "INFO"
}
]
}
},
"test_levels": {
"Nameserver": {
"OTHER_TAG": "ERROR"
}
},
"test_cases": [
"Zone02"
]
}
);
Readonly my $EXAMPLE_PROFILE_3 => qq(
{
"resolver": {
"source4": "",
"source6": ""
}
}
);
subtest 'new() returns a new profile every time' => sub {
my $profile1 = Zonemaster::Engine::Profile->new;
my $profile2 = Zonemaster::Engine::Profile->new;
$profile1->set( 'net.ipv4', 1 );
is $profile2->get( 'net.ipv4' ), undef, 'net.ipv4 is unaffected by update to another instance';
};
subtest 'new() returns a profile with all properties unset' => sub {
my $profile = Zonemaster::Engine::Profile->new;
for my $property ( Zonemaster::Engine::Profile->all_properties ) {
is $profile->get( $property ), undef, "$property is unset";
}
};
subtest 'default() returns a new profile every time' => sub {
my $profile1 = Zonemaster::Engine::Profile->default;
$profile1->set( 'net.ipv4', 1 );
my $profile2 = Zonemaster::Engine::Profile->default;
$profile2->set( 'net.ipv4', 0 );
is $profile1->get( 'net.ipv4' ), 1, 'net.ipv4 is unaffected by update to another instance';
};
subtest 'default() returns a profile with all properties set' => sub {
my $profile = Zonemaster::Engine::Profile->default;
for my $property ( Zonemaster::Engine::Profile->all_properties ) {
ok defined( $profile->get( $property ) ), "$property is set";
}
};
subtest 'from_json() returns a new profile every time' => sub {
my $profile1 = Zonemaster::Engine::Profile->from_json( "{}" );
my $profile2 = Zonemaster::Engine::Profile->from_json( "{}" );
$profile1->set( 'net.ipv4', 1 );
is $profile2->get( 'net.ipv4' ), undef, 'net.ipv4 is unaffected by update to another instance';
};
subtest 'from_json("{}") returns a profile with all properties unset' => sub {
my $profile = Zonemaster::Engine::Profile->from_json( "{}" );
for my $property ( Zonemaster::Engine::Profile->all_properties ) {
is $profile->get( $property ), undef, "$property is unset";
}
};
subtest 'from_json() parses values from a string' => sub {
my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
is $profile->get( 'resolver.defaults.usevc' ), 1, 'resolver.defaults.usevc was parsed from JSON';
is $profile->get( 'resolver.defaults.recurse' ), 1, 'resolver.defaults.recurse was parsed from JSON';
is $profile->get( 'resolver.defaults.igntc' ), 0, 'resolver.defaults.igntc was parsed from JSON';
is $profile->get( 'resolver.defaults.fallback' ), 1, 'resolver.defaults.fallback was parsed from JSON';
is $profile->get( 'net.ipv4' ), 1, 'net.ipv4 was parsed from JSON';
is $profile->get( 'net.ipv6' ), 0, 'net.ipv6 was parsed from JSON';
is $profile->get( 'no_network' ), 1, 'no_network was parsed from JSON';
is $profile->get( 'resolver.defaults.retry' ), 123, 'resolver.defaults.retry was parsed from JSON';
is $profile->get( 'resolver.defaults.retrans' ), 234, 'resolver.defaults.retrans was parsed from JSON';
is $profile->get( 'resolver.source4' ), '192.0.2.53', 'resolver.source4 was parsed from JSON';
is $profile->get( 'resolver.source6' ), '2001:db8::42', 'resolver.source6 was parsed from JSON';
eq_or_diff $profile->get( 'asn_db.style' ), 'cymru', 'asn_db.style was parsed from JSON';
eq_or_diff $profile->get( 'asn_db.sources' ), { cymru => ["asn1.example.com", "asn2.example.com"] }, 'asn_db.sources was parsed from JSON';
eq_or_diff $profile->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'logfilter was parsed from JSON';
eq_or_diff $profile->get( 'test_levels' ), { Zone => { TAG => 'INFO' } }, 'test_levels was parsed from JSON';
eq_or_diff $profile->get( 'test_cases' ), ['Zone01'], 'test_cases was parsed from JSON';
eq_or_diff $profile->get( 'cache' ), { redis => { server => '127.0.0.1:6379', expire => 3600 } }, 'cache was parsed from JSON';
};
subtest 'from_json() parses sentinel values from a string' => sub {
my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_3 );
is $profile->get( 'resolver.source4' ), '', 'resolver.source4 was parsed from JSON';
is $profile->get( 'resolver.source6' ), '', 'resolver.source6 was parsed from JSON';
};
subtest 'from_json() dies on illegal paths' => sub {
throws_ok { Zonemaster::Engine::Profile->from_json( '{"foobar":1}' ) } qr/^.*Unknown property .*/, 'foobar';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"net":1}' ) } qr/^.*Unknown property .*/, 'net';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"net":{"foobar":1}}' ) } qr/^.*Unknown property .*/, 'net.foobar';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":1}' ) } qr/^.*Unknown property .*/, 'resolver';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":1}}' ) } qr/^.*Unknown property .*/, 'resolver.defaults';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"foobar":1}}}' ) } qr/^.*Unknown property .*/, 'resolver.defaults.foobar';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"dnssec":1}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.dnssec';
throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"edns_size":1}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.edns_size';
};
subtest 'from_json() dies on illegal values' => sub {
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"usevc":0}}}' ); } "checks type of resolver.defaults.usevc";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"recurse":0}}}' ); } "checks type of resolver.defaults.recurse";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"igntc":1}}}' ); } "checks type of resolver.defaults.igntc";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"fallback":0}}}' ); } "checks type of resolver.defaults.fallback";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"net":{"ipv4":1}}' ); } "checks type of net.ipv4";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"net":{"ipv6":0}}' ); } "checks type of net.ipv6";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"no_network":1}' ); } "checks type of no_network";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retry":0}}}' ); } "checks lower bound of resolver.defaults.retry";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retry":256}}}' ); } "checks upper bound of resolver.defaults.retry";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retry":1.5}}}' ); } "checks type of resolver.defaults.retry";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retrans":0}}}' ); } "checks lower bound of resolver.defaults.retrans";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retrans":256}}}' ); } "checks upper bound of resolver.defaults.retrans";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"retrans":1.5}}}' ); } "checks type of resolver.defaults.retrans";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"asn_db":{"style":["noreply@example"]}' ); } "checks type of asndb.style";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"asn_db":{"sources":["noreply@example"]}' ); } "checks type of asndb.sources";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"logfilter":[]}' ); } "checks type of logfilter";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"test_levels":[]}' ); } "checks type of test_levels";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"test_cases":{}}' ); } "checks type of test_cases";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"cache":[]}' ); } "checks type of cache";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source4":"example.com"}}' ); } "checks type of resolver.source4";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source4":"2001:db8::42"}}' ); } "checks type of resolver.source4 (only IPv4)";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source6":"example.com"}}' ); } "checks type of resolver.source6";
dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"source6":"192.0.2.53"}}' ); } "checks type of resolver.source6 (only IPv6)";
};
subtest 'from_yaml() equals from_json() for a similar profile' => sub {
my $profile_json = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
my $profile_yaml = Zonemaster::Engine::Profile->from_yaml( $EXAMPLE_PROFILE_1_YAML );
is_deeply( $profile_yaml, $profile_json, 'same JSON and YAML profile' );
};
subtest 'get() returns 1 for true' => sub {
my $profile = Zonemaster::Engine::Profile->from_json(
'{
"resolver": {
"defaults": {
"usevc": true,
"recurse": true,
"igntc": true,
"fallback": true
}
},
"net": {
"ipv4": true,
"ipv6": true
},
"no_network": true
}'
);
is $profile->get( 'resolver.defaults.usevc' ), 1, "returns 1 for true resolver.defaults.usevc";
is $profile->get( 'resolver.defaults.recurse' ), 1, "returns 1 for true resolver.defaults.recurse";
is $profile->get( 'resolver.defaults.igntc' ), 1, "returns 1 for true resolver.defaults.igntc";
is $profile->get( 'resolver.defaults.fallback' ), 1, "returns 1 for true resolver.defaults.fallback";
is $profile->get( 'net.ipv4' ), 1, "returns 1 for true net.ipv4";
is $profile->get( 'net.ipv6' ), 1, "returns 1 for true net.ipv6";
is $profile->get( 'no_network' ), 1, "returns 1 for true no_network";
};
subtest 'get() returns 0 for false' => sub {
my $profile = Zonemaster::Engine::Profile->from_json(
'{
"resolver": {
"defaults": {
"usevc": false,
"recurse": false,
"igntc": false,
"fallback": false
}
},
"net": {
"ipv4": false,
"ipv6": false
},
"no_network": false
}'
);
is $profile->get( 'resolver.defaults.usevc' ), 0, "returns 0 for false resolver.defaults.usevc";
is $profile->get( 'resolver.defaults.recurse' ), 0, "returns 0 for false resolver.defaults.recurse";
is $profile->get( 'resolver.defaults.igntc' ), 0, "returns 0 for false resolver.defaults.igntc";
is $profile->get( 'resolver.defaults.fallback' ), 0, "returns 0 for false resolver.defaults.fallback";
is $profile->get( 'net.ipv4' ), 0, "returns 0 for false net.ipv4";
is $profile->get( 'net.ipv6' ), 0, "returns 0 for false net.ipv6";
is $profile->get( 'no_network' ), 0, "returns 0 for false no_network";
};
subtest 'get() returns deep copies of properties with complex types' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asn_db.sources', {} );
$profile->set( 'logfilter', {} );
$profile->set( 'test_levels', {} );
$profile->set( 'test_cases', [] );
$profile->set( 'cache', {} );
$profile->get( 'asn_db.sources' )->{cymru} = ['asn1.example.com', 'asn2.example.com'];
push @{ $profile->get( 'test_cases' ) }, 'Zone01';
$profile->get( 'logfilter' )->{Zone} = {};
$profile->get( 'test_levels' )->{Zone}{TAG} = 'INFO';
$profile->get( 'cache' )->{redis}{server} = '127.0.0.1:6379';
eq_or_diff $profile->get( 'asn_db.sources' ), {}, 'get(asn_db.sources) returns a deep copy';
eq_or_diff $profile->get( 'logfilter' ), {}, 'get(logfilter) returns a deep copy';
eq_or_diff $profile->get( 'test_levels' ), {}, 'get(test_levels) returns a deep copy';
eq_or_diff $profile->get( 'test_cases' ), [], 'get(test_cases) returns a deep copy';
eq_or_diff $profile->get( 'cache' ), {}, 'get(cache) returns a deep copy';
};
subtest 'get() dies if the given property name is invalid' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asn_db.style', 'cymru' );
$profile->set( 'asn_db.sources', { cymru => ['asn1.example.com', 'asn2.example.com'] } );
$profile->set( 'logfilter', { Zone => {} } );
$profile->set( 'test_levels', { Zone => { TAG => 'INFO' } } );
$profile->set( 'test_cases', ['Zone01'] );
$profile->set( 'cache', { redis => { server => '127.0.0.1:6379' } } );
throws_ok { $profile->get( 'net' ) } qr/^.*Unknown property .*/, 'net';
throws_ok { $profile->get( 'net.foobar' ) } qr/^.*Unknown property .*/, 'net.foobar';
throws_ok { $profile->get( 'resolver.defaults' ) } qr/^.*Unknown property .*/, 'resolver.defaults';
throws_ok { $profile->get( 'resolver' ) } qr/^.*Unknown property .*/, 'resolver';
throws_ok { $profile->get( 'asn_db.fake' ) } qr/^.*Unknown property .*/, 'asn_db.fake';
throws_ok { $profile->get( 'logfilter.Zone' ) } qr/^.*Unknown property .*/, 'logfilter.Zone';
throws_ok { $profile->get( 'test_levels.Zone' ) } qr/^.*Unknown property .*/, 'test_levels.Zone';
throws_ok { $profile->get( 'test_cases.Zone01' ) } qr/^.*Unknown property .*/, 'test_cases.Zone01';
throws_ok { $profile->get( 'cache.redis' ) } qr/^.*Unknown property .*/, 'cache.redis';
throws_ok { $profile->get( 'resolver.defaults.dnssec' ) } qr/^.*Unknown property .*/, 'resolver.defaults.dnssec';
throws_ok { $profile->get( 'resolver.defaults.edns_size' ) } qr/^.*Unknown property .*/, 'resolver.defaults.edns_size';
};
subtest 'set() inserts values for unset properties' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.usevc', 1 );
$profile->set( 'resolver.defaults.recurse', 1 );
$profile->set( 'resolver.defaults.igntc', 0 );
$profile->set( 'resolver.defaults.fallback', 1 );
$profile->set( 'net.ipv4', 0 );
$profile->set( 'net.ipv6', 1 );
$profile->set( 'no_network', 0 );
$profile->set( 'resolver.defaults.retry', 123 );
$profile->set( 'resolver.defaults.retrans', 234 );
$profile->set( 'resolver.source4', '192.0.2.53' );
$profile->set( 'resolver.source6', '2001:db8::42' );
$profile->set( 'asn_db.style', 'cymru' );
$profile->set( 'asn_db.sources', { cymru => ['asn1.example.com', 'asn2.example.com'] } );
$profile->set( 'logfilter', { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } } );
$profile->set( 'test_levels', { Zone => { TAG => 'INFO' } } );
$profile->set( 'test_cases', ['Zone01'] );
$profile->set( 'cache', { redis => { server => '127.0.0.1:6379', expire => 3600 } } );
is $profile->get( 'resolver.defaults.usevc' ), 1, 'resolver.defaults.usevc can be given a value when unset';
is $profile->get( 'resolver.defaults.recurse' ), 1, 'resolver.defaults.recurse can be given a value when unset';
is $profile->get( 'resolver.defaults.igntc' ), 0, 'resolver.defaults.igntc can be given a value when unset';
is $profile->get( 'resolver.defaults.fallback' ), 1, 'resolver.defaults.fallback can be given a value when unset';
is $profile->get( 'net.ipv4' ), 0, 'net.ipv4 can be given a value when unset';
is $profile->get( 'net.ipv6' ), 1, 'net.ipv6 can be given a value when unset';
is $profile->get( 'no_network' ), 0, 'no_network can be given a value when unset';
is $profile->get( 'resolver.defaults.retry' ), 123, 'resolver.defaults.retry can be given a value when unset';
is $profile->get( 'resolver.defaults.retrans' ), 234, 'resolver.defaults.retrans can be given a value when unset';
is $profile->get( 'resolver.source4' ), '192.0.2.53', 'resolver.source4 can be given a value when unset';
is $profile->get( 'resolver.source6' ), '2001:db8::42', 'resolver.source6 can be given a value when unset';
eq_or_diff $profile->get( 'asn_db.style' ), 'cymru', 'asn_db.style can be given a value when unset';
eq_or_diff $profile->get( 'asn_db.sources' ), { cymru => ['asn1.example.com', 'asn2.example.com'] }, 'asn_db.sources can be given a value when unset';
eq_or_diff $profile->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'logfilter can be given a value when unset';
eq_or_diff $profile->get( 'test_levels' ), { Zone => { TAG => 'INFO' } },
'test_levels can be given a value when unset';
eq_or_diff $profile->get( 'test_cases' ), ['Zone01'], 'test_cases can be given a value when unset';
eq_or_diff $profile->get( 'cache' ), { redis => { server => '127.0.0.1:6379', expire => 3600 } },
'cache can be given a value when unset';
};
subtest 'set() updates values for set properties' => sub {
my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
$profile->set( 'resolver.defaults.usevc', 0 );
$profile->set( 'resolver.defaults.recurse', 0 );
$profile->set( 'resolver.defaults.igntc', 1 );
$profile->set( 'resolver.defaults.fallback', 0 );
$profile->set( 'net.ipv4', 0 );
$profile->set( 'net.ipv6', 1 );
$profile->set( 'no_network', 0 );
$profile->set( 'resolver.defaults.retry', 99 );
$profile->set( 'resolver.defaults.retrans', 88 );
$profile->set( 'resolver.source4', '198.51.100.53' );
$profile->set( 'resolver.source6', '2001:db8::cafe' );
$profile->set( 'asn_db.style', 'ripe' );
$profile->set( 'asn_db.sources', { ripe => ['asn3.example.com', 'asn4.example.com'] } );
$profile->set( 'logfilter', { Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } } );
$profile->set( 'test_levels', { Nameserver => { OTHER_TAG => 'ERROR' } } );
$profile->set( 'test_cases', ['Zone02'] );
$profile->set( 'cache', { redis => { server => '127.0.0.2:6379', expire => 7200 } } );
is $profile->get( 'resolver.defaults.usevc' ), 0, 'resolver.defaults.usevc was updated';
is $profile->get( 'resolver.defaults.recurse' ), 0, 'resolver.defaults.recurse was updated';
is $profile->get( 'resolver.defaults.igntc' ), 1, 'resolver.defaults.igntc was updated';
is $profile->get( 'net.ipv4' ), 0, 'net.ipv4 was updated';
is $profile->get( 'net.ipv6' ), 1, 'net.ipv6 was updated';
is $profile->get( 'no_network' ), 0, 'no_network was updated';
is $profile->get( 'resolver.defaults.retry' ), 99, 'resolver.defaults.retry was updated';
is $profile->get( 'resolver.defaults.retrans' ), 88, 'resolver.defaults.retrans was updated';
is $profile->get( 'resolver.source4' ), '198.51.100.53', 'resolver.source4 was updated';
is $profile->get( 'resolver.source6' ), '2001:db8::cafe', 'resolver.source6 was updated';
eq_or_diff $profile->get( 'asn_db.style' ), 'ripe', 'asn_db.style was updated';
eq_or_diff $profile->get( 'asn_db.sources' ), { ripe => ['asn3.example.com', 'asn4.example.com'] }, 'asn_db.sources was updated';
eq_or_diff $profile->get( 'logfilter' ),
{ Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } }, 'logfilter was updated';
eq_or_diff $profile->get( 'test_levels' ), { Nameserver => { OTHER_TAG => 'ERROR' } }, 'test_levels was updated';
eq_or_diff $profile->get( 'test_cases' ), ['Zone02'], 'test_cases was updated';
eq_or_diff $profile->get( 'cache' ), { redis => { server => '127.0.0.2:6379', expire => 7200 } },
'cache was updated';
};
subtest 'set() dies on attempts to unset properties' => sub {
my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
for my $property ( Zonemaster::Engine::Profile->all_properties ) {
throws_ok { $profile->set( $property, undef ); } qr/^.* can not be undef/, "dies on attempt to unset $property";
}
};
subtest 'set() dies if the given property name is invalid' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asn_db.style', 'cymru' );
$profile->set( 'asn_db.sources', { cymru => ['asn1.example.com', 'asn2.example.com'] } );
$profile->set( 'logfilter', { Zone => {} } );
$profile->set( 'test_levels', { Zone => {} } );
$profile->set( 'test_cases', ['Zone01'] );
$profile->set( 'cache', { redis => { server => '127.0.0.1:6379' } } );
throws_ok { $profile->set( 'net', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for net';
throws_ok { $profile->set( 'net.foobar', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for net.foobar';
throws_ok { $profile->set( 'resolver', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for resolver';
throws_ok { $profile->set( 'resolver.defaults', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for resolver.defaults';
throws_ok { $profile->set( 'asn_db', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for asn_db';
throws_ok { $profile->set( 'asn_db.fake', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for asn_db.fake';
throws_ok { $profile->set( 'logfilter.Zone', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for logfilter.Zone';
throws_ok { $profile->set( 'test_levels.Zone', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for test_levels.Zone';
throws_ok { $profile->set( 'test_cases.Zone01', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for test_cases.Zone01';
throws_ok { $profile->set( 'cache.redis', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for cache.redis';
throws_ok { $profile->set( 'resolver.defaults.dnssec', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for resolver.defaults.dnssec';
throws_ok { $profile->set( 'resolver.defaults.edns_size', 1 ) } qr/^.*Unknown property .*/, 'dies on attempt to set a value for resolver.defaults.edns_size';
};
subtest 'set() dies on illegal value' => sub {
my $profile = Zonemaster::Engine::Profile->new;
dies_ok { $profile->set( 'resolver.defaults.retry', 0 ); } 'checks lower bound of resolver.defaults.retry';
dies_ok { $profile->set( 'resolver.defaults.retry', 256 ); } 'checks upper bound of resolver.defaults.retry';
dies_ok { $profile->set( 'resolver.defaults.retry', 1.5 ); } 'checks type of resolver.defaults.retry';
dies_ok { $profile->set( 'resolver.defaults.retrans', 0 ); } 'checks lower bound of resolver.defaults.retrans';
dies_ok { $profile->set( 'resolver.defaults.retrans', 256 ); } 'checks upper bound of resolver.defaults.retrans';
dies_ok { $profile->set( 'resolver.defaults.retrans', 1.5 ); } 'checks type of resolver.defaults.retrans';
dies_ok { $profile->set( 'resolver.source4', 'example.com' ); } 'resolver.source4 rejects domain name string';
dies_ok { $profile->set( 'resolver.source4', ['192.0.2.53'] ); } 'resolver.source4 rejects arrayref';
dies_ok { $profile->set( 'resolver.source6', 'example.com' ); } 'resolver.source6 rejects domain name string';
dies_ok { $profile->set( 'resolver.source6', ['2001:db8::42'] ); } 'resolver.source6 rejects arrayref';
dies_ok { $profile->set( 'asn_db.style', ['noreply@example.com'] ); } 'checks type of asn_db.style';
dies_ok { $profile->set( 'asn_db.sources', ['noreply@example.com'] ); } 'checks type of asn_db.sources';
dies_ok { $profile->set( 'logfilter', [] ); } 'checks type of logfilter';
dies_ok { $profile->set( 'test_levels', [] ); } 'checks type of test_levels';
dies_ok { $profile->set( 'test_cases', {} ); } 'checks type of test_cases';
dies_ok { $profile->set( 'cache', [] ); } 'checks type of cache';
};
subtest 'set() accepts sentinel values' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source4', '' );
is $profile->get( 'resolver.source4' ), '', 'resolver.source4 was updated';
$profile->set( 'resolver.source6', '' );
is $profile->get( 'resolver.source6' ), '', 'resolver.source6 was updated';
};
subtest 'set() uses standard truthiness rules for boolean properties' => sub {
my $profile = Zonemaster::Engine::Profile->new;
subtest 'values considered false' => sub {
$profile->set( 'no_network', 0 );
ok !$profile->get( 'no_network' ), 'the number 0';
$profile->set( 'no_network', "" );
ok !$profile->get( 'no_network' ), 'the empty string';
$profile->set( 'no_network', "0" );
ok !$profile->get( 'no_network' ), 'the string that contains a single 0 digit';
};
subtest 'values considered true' => sub {
$profile->set( 'no_network', 1 );
ok $profile->get( 'no_network' ), 'any non-0 number';
$profile->set( 'no_network', " " );
ok $profile->get( 'no_network' ), 'the string with a space in it';
$profile->set( 'no_network', "00" );
ok $profile->get( 'no_network' ), 'two or more 0 characters in a string';
$profile->set( 'no_network', "0\n" );
ok $profile->get( 'no_network' ), 'a 0 followed by a newline';
$profile->set( 'no_network', "true" );
ok $profile->get( 'no_network' ), 'the string "true"';
$profile->set( 'no_network', "false" );
ok $profile->get( 'no_network' ), 'yes, even the string "false"';
};
};
subtest 'merge() with a profile with all properties unset' => sub {
my $profile1 = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
my $profile2 = Zonemaster::Engine::Profile->new;
$profile1->merge( $profile2 );
is $profile1->get( 'resolver.defaults.usevc' ), 1, 'keeps value of resolver.defaults.usevc';
is $profile1->get( 'resolver.defaults.recurse' ), 1, 'keeps value of resolver.defaults.recurse';
is $profile1->get( 'resolver.defaults.igntc' ), 0, 'keeps value of resolver.defaults.igntc';
is $profile1->get( 'resolver.defaults.fallback' ), 1, 'keeps value of resolver.defaults.fallback';
is $profile1->get( 'net.ipv4' ), 1, 'keeps value of net.ipv4';
is $profile1->get( 'net.ipv6' ), 0, 'keeps value of net.ipv6';
is $profile1->get( 'no_network' ), 1, 'keeps value of no_network';
is $profile1->get( 'resolver.defaults.retry' ), 123, 'keeps value of resolver.defaults.retry';
is $profile1->get( 'resolver.defaults.retrans' ), 234, 'keeps value of resolver.defaults.retrans';
is $profile1->get( 'resolver.source4' ), '192.0.2.53', 'keeps value of resolver.source4';
is $profile1->get( 'resolver.source6' ), '2001:db8::42', 'keeps value of resolver.source6';
eq_or_diff $profile1->get( 'asn_db.style' ), 'cymru', 'keeps value of asn_db.style';
eq_or_diff $profile1->get( 'asn_db.sources' ), { cymru => ['asn1.example.com', 'asn2.example.com'] }, 'keeps value of asn_db.sources';
eq_or_diff $profile1->get( 'logfilter' ), { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } },
'keeps value of logfilter';
eq_or_diff $profile1->get( 'test_levels' ), { Zone => { TAG => 'INFO' } }, 'keeps value of test_levels';
eq_or_diff $profile1->get( 'test_cases' ), ['Zone01'], 'keeps value of test_cases';
eq_or_diff $profile1->get( 'cache' ), { redis => { server => '127.0.0.1:6379', expire => 3600 } }, 'keeps value of cache';
};
subtest 'merge() with a profile with all properties set' => sub {
my $profile1 = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
my $profile2 = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_2 );
$profile1->merge( $profile2 );
is $profile1->get( 'resolver.defaults.usevc' ), 0, 'updates resolver.defaults.usevc';
is $profile1->get( 'resolver.defaults.recurse' ), 0, 'updates resolver.defaults.recurse';
is $profile1->get( 'resolver.defaults.igntc' ), 1, 'updates resolver.defaults.igntc';
is $profile1->get( 'resolver.defaults.fallback' ), 0, 'updates resolver.defaults.fallback';
is $profile1->get( 'net.ipv4' ), 0, 'updates net.ipv4';
is $profile1->get( 'net.ipv6' ), 1, 'updates net.ipv6';
is $profile1->get( 'no_network' ), 0, 'updates no_network';
is $profile1->get( 'resolver.defaults.retry' ), 99, 'updates resolver.defaults.retry';
is $profile1->get( 'resolver.defaults.retrans' ), 88, 'updates resolver.defaults.retrans';
is $profile1->get( 'resolver.source4' ), '198.51.100.53', 'updates resolver.source4';
is $profile1->get( 'resolver.source6' ), '2001:db8::cafe', 'updates resolver.source6';
eq_or_diff $profile1->get( 'asn_db.style' ), 'ripe', 'updates asn_db.style';
eq_or_diff $profile1->get( 'asn_db.sources' ), { ripe => ['asn3.example.com', 'asn4.example.com'] }, 'updates asn_db.sources';
eq_or_diff $profile1->get( 'logfilter' ),
{ Nameserver => { OTHER_TAG => [ { when => { apples => 1 }, set => 'INFO' } ] } }, 'updates logfilter';
eq_or_diff $profile1->get( 'test_levels' ), { Nameserver => { OTHER_TAG => 'ERROR' } }, 'updates test_levels';
eq_or_diff $profile1->get( 'test_cases' ), ['Zone02'], 'updates test_cases';
eq_or_diff $profile1->get( 'cache' ), { redis => { server => '127.0.0.2:6379', expire => 7200 } }, 'updates cache';
};
subtest 'merge() does not update the other profile' => sub {
my $profile1 = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 );
my $profile2 = Zonemaster::Engine::Profile->new;
$profile1->merge( $profile2 );
is $profile2->get( 'resolver.defaults.usevc' ), undef, 'resolver.defaults.usevc was untouched in other';
is $profile2->get( 'resolver.defaults.retrans' ), undef, 'resolver.defaults.retrans was untouched in other';
is $profile2->get( 'resolver.defaults.recurse' ), undef, 'resolver.defaults.recurse was untouched in other';
is $profile2->get( 'resolver.defaults.retry' ), undef, 'resolver.defaults.retry was untouched in other';
is $profile2->get( 'resolver.defaults.igntc' ), undef, 'resolver.defaults.igntc was untouched in other';
is $profile2->get( 'resolver.defaults.fallback' ), undef, 'resolver.defaults.fallback was untouched in other';
is $profile2->get( 'resolver.source4' ), undef, 'resolver.source4 was untouched in other';
is $profile2->get( 'resolver.source6' ), undef, 'resolver.source6 was untouched in other';
is $profile2->get( 'net.ipv4' ), undef, 'net.ipv4 was untouched in other';
is $profile2->get( 'net.ipv6' ), undef, 'net.ipv6 was untouched in other';
is $profile2->get( 'no_network' ), undef, 'no_network was untouched in other';
is $profile2->get( 'asn_db.style' ), undef, 'asn_db.style was untouched in other';
is $profile2->get( 'asn_db.sources' ), undef, 'asn_db.sources was untouched in other';
is $profile2->get( 'logfilter' ), undef, 'logfilter was untouched in other';
is $profile2->get( 'test_levels' ), undef, 'test_levels was untouched in other';
is $profile2->get( 'test_cases' ), undef, 'test_cases was untouched in other';
is $profile2->get( 'cache' ), undef, 'cache was untouched in other';
};
subtest 'to_json() serializes each property' => sub {
subtest 'resolver.defaults.usevc' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.usevc', 1 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"usevc":true}}}' );
};
subtest 'resolver.defaults.recurse' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.recurse', 1 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"recurse":true}}}' );
};
subtest 'resolver.defaults.igntc' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.igntc', 0 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"igntc":false}}}' );
};
subtest 'resolver.defaults.fallback' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.fallback', 0 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"fallback":false}}}' );
};
subtest 'net.ipv4' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'net.ipv4', 1 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"net":{"ipv4":true}}' );
};
subtest 'net.ipv6' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'net.ipv6', 0 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"net":{"ipv6":false}}' );
};
subtest 'no_network' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'no_network', 1 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"no_network":true}' );
};
subtest 'resolver.defaults.retry' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.retry', 123 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"retry":123}}}' );
};
subtest 'resolver.defaults.retrans' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.defaults.retrans', 234 );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"retrans":234}}}' );
};
subtest 'resolver.source4' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source4', '192.0.2.53' );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"source4":"192.0.2.53"}}' );
};
subtest 'resolver.source6' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source6', '2001:db8::42' );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"source6":"2001:db8::42"}}' );
};
subtest 'resolver.source6 sentinel value' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'resolver.source6', '' );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( qq({"resolver":{"source6":""}}) );
};
subtest 'asn_db.style' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asn_db.style', 'cymru' );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"asn_db":{"style": "cymru"}}' );
};
subtest 'asn_db.sources' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'asn_db.sources', { cymru => ['asn1.example.com','asn2.example.com'] } );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"asn_db":{"sources": {"cymru": ["asn1.example.com","asn2.example.com"]}}} ' );
};
subtest 'test_cases' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'test_cases', ['Zone01'] );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"test_cases":["Zone01"]}' );
};
subtest 'test_levels' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'test_levels', { Zone => { TAG => 'INFO' } } );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ), decode_json( '{"test_levels":{"Zone":{"TAG":"INFO"}}}' );
};
subtest 'logfilter' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'logfilter', { Zone => { TAG => [ { when => { bananas => 0 }, set => 'WARNING' } ] } } );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ),
decode_json( '{"logfilter":{"Zone":{"TAG":[{"when":{"bananas":0},"set":"WARNING"}]}}}' );
};
subtest 'cache' => sub {
my $profile = Zonemaster::Engine::Profile->new;
$profile->set( 'cache', { redis => { server => '127.0.0.1:6379', expire => 3600 } } );
my $json = $profile->to_json;
eq_or_diff decode_json( $json ),
decode_json( '{"cache":{"redis":{"server":"127.0.0.1:6379","expire":3600}}}' );
};
};
subtest 'effective() is initially equivalent to default()' => sub {
my $json0 = Zonemaster::Engine::Profile->default->to_json;
my $json1 = Zonemaster::Engine::Profile->effective->to_json;
eq_or_diff decode_json( $json1 ), decode_json( $json0 );
};
subtest 'effective() returns the same profile every time' => sub {
my $profile1 = Zonemaster::Engine::Profile->effective;
$profile1->set( 'resolver.defaults.retry', 111 );
my $profile2 = Zonemaster::Engine::Profile->effective;
$profile2->set( 'resolver.defaults.retry', 222 );
is $profile1->get( 'resolver.defaults.retry' ), 222;
};
done_testing;