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::Backend::Config' ) || print "Bail out!\n";
}
done_testing;

View File

@@ -0,0 +1,197 @@
package TestUtil;
use strict;
use warnings;
use Test::More;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
=head1 NAME
TestUtil - a set of methods to ease Zonemaster::Backend unit testing
=head1 SYNOPSIS
Because this package lies in the testing folder C<t/> and that folder is
unknown to the include path @INC, it can be including using the following code:
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
Explicitely load any dependencies to Zonemaster::Backend::RPCAPI or
Zonemaster::Backend::TestAgent modules with
use TestUtil qw( RPCAPI TestAgent );
=head1 ENVIRONMENT
=head2 TARGET
Set the database to use.
Can be C<SQLite>, C<MySQL> or C<PostgreSQL>.
Default to C<SQLite>.
=head2 ZONEMASTER_RECORD
If set, the data from the test is recorded to a file. Otherwise the data is
loaded from a file.
=cut
# Use the TARGET environment variable to set the database to use
# default to SQLite
my $db_backend = Zonemaster::Backend::Config->check_db( $ENV{TARGET} || 'SQLite' );
note "database: $db_backend";
sub import {
my ( $class, @args ) = @_;
if ( grep { $_ eq 'RPCAPI' } @args ) {
require Zonemaster::Backend::RPCAPI;
Zonemaster::Backend::RPCAPI->import();
}
if ( grep { $_ eq 'TestAgent' } @args ) {
require Zonemaster::Backend::TestAgent;
Zonemaster::Backend::TestAgent->import();
}
}
sub db_backend {
return $db_backend;
}
sub restore_datafile {
my ( $datafile ) = @_;
if ( not $ENV{ZONEMASTER_RECORD} ) {
die q{Stored data file missing} if not -r $datafile;
Zonemaster::Engine->preload_cache( $datafile );
Zonemaster::Engine->profile->set( q{no_network}, 1 );
} else {
diag "recording";
}
}
sub save_datafile {
my ( $datafile ) = @_;
if ( $ENV{ZONEMASTER_RECORD} ) {
Zonemaster::Engine->save_cache( $datafile );
}
}
sub prepare_db {
my ( $db ) = @_;
$db->drop_tables();
$db->create_schema();
}
sub init_db {
my ( $config ) = @_;
my $dbclass = Zonemaster::Backend::DB->get_db_class( $db_backend );
my $db = $dbclass->from_config( $config );
prepare_db( $db );
return $db;
}
sub create_rpcapi {
my ( $config ) = @_;
my $rpcapi;
eval {
$rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $db_backend,
config => $config,
}
);
};
if ( $@ ) {
diag explain( $@ );
BAIL_OUT( 'Could not connect to database' );
}
if ( not $rpcapi->isa('Zonemaster::Backend::RPCAPI' ) ) {
BAIL_OUT( 'Not a Zonemaster::Backend::RPCAPI object' );
}
prepare_db( $rpcapi->{db} );
return $rpcapi;
}
sub create_testagent {
my ( $config ) = @_;
my $agent = Zonemaster::Backend::TestAgent->new(
{
dbtype => "$db_backend",
config => $config
}
);
if ( not $agent->isa('Zonemaster::Backend::TestAgent' ) ) {
BAIL_OUT( 'Not a Zonemaster::Backend::TestAgent object' );
}
return $agent;
}
=head1 METHODS
=over
=item db_backend()
Returns the name of the currently used database engine. This value is set via
the TARGET environment variable.
=item restore_datafile($datafile)
If the ZONEMASTER_RECORD environment variable is unset, the data from
C<$datafile> is used for all the current tests.
=item save_datafile($datafile)
If the ZONEMASTER_RECORD environment variable is set, the data from the current
tests are stored to C<$datafile>.
=item prepare_db($db)
Recreate all tables anew for the associated C<$db>.
=item init_db($config)
Returns a new Zonemaster::Backend::DB object using the provided C<$config>
file.
Database tables are dropped and created anew.
=item create_rpcapi($config)
Returns a new Zonemaster::Backend::RPCAPI object using the provided C<$config>
file.
Database tables are dropped and created anew.
=item create_testagent($config)
Returns a new Zonemaster::Backend::TestAgent object using the provided
C<$config> file.
=back
=cut
1;

View File

@@ -0,0 +1,361 @@
use strict;
use warnings;
use 5.14.2;
use Data::Dumper;
use File::Temp qw[tempdir];
use POSIX qw( strftime );
use Time::Local qw( timelocal_modern );
use Test::Exception;
use Test::More; # see done_testing()
use Test::Differences;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( RPCAPI );
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = <<EOF;
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
[PUBLIC PROFILES]
test_profile=$t_path/test_profile.json
EOF
my $user = {
username => 'user',
api_key => 'key'
};
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'test_profile',
};
# Create Zonemaster::Backend::RPCAPI object
sub init_backend {
my ( $config ) = @_;
my $rpcapi = TestUtil::create_rpcapi( $config );
# create a user
$rpcapi->add_api_user( $user );
return $rpcapi;
}
sub to_timestamp {
my ( $date ) = @_;
my ( $year, $month, $day, $hour, $min, $sec ) = split( /[\s:-]+/, $date );
my $time = timelocal_modern( $sec, $min, $hour, $day, $month-1, $year );
return $time;
}
sub check_tolerance {
my ( $ref_time, $msg ) = @_;
my $current_time = strftime "%Y-%m-%d %H:%M:%S", gmtime( time() );
my $delta = abs( to_timestamp($current_time) - to_timestamp($ref_time) );
my $tolerance = 60; # 1 minute is tolerable between ret_time and current_time
cmp_ok( $delta, '<=', $tolerance, $msg);
}
subtest 'RPCAPI add_batch_job' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = ( 'afnic.fr' );
my $res = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $res, 1, 'correct batch job id returned' );
subtest 'table "batch_jobs" contains an entry' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM batch_jobs ] );
is( $count, 1, 'one row in table' );
my ( $id, $username, $created_at ) = $dbh->selectrow_array( q[ SELECT * FROM batch_jobs ]);
is( $id, 1, 'first batch id is 1' );
is( $username, $user->{username}, 'correct batch user' );
ok( $created_at, 'defined creation time' );
check_tolerance( $created_at, 'creation time in tolerance zone' );
};
subtest 'table "test_results" contains an entry' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM test_results ] );
is( $count, 1, 'one row in table' );
my ( $hash_id, $domain, $batch_id, $created_at, $started_at, $ended_at, $params ) = $dbh->selectrow_array(
q[
SELECT
hash_id,
domain,
batch_id,
created_at,
started_at,
ended_at,
params
FROM test_results
]
);
is( length($hash_id), 16, 'correct hash_id length' );
is( $domain, $domains[0], 'correct domain' );
is( $batch_id, 1, 'correct batch_id' );
ok( $created_at, 'defined creation time' );
check_tolerance( $created_at, 'creation time in tolerance zone' );
ok( ! defined $started_at, 'undefined start time' );
ok( ! defined $ended_at, 'undefined end time' );
};
};
subtest 'RPCAPI batch_status' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
subtest 'batch job exists' => sub {
my @domains = ( 'afnic.fr' );
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 1, 'correct batch job id returned' );
my $res = $rpcapi->batch_status( { batch_id => $batch_id } );
is( $res->{waiting_count}, scalar @domains, 'correct number of runninng tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
ok( !exists $res->{waiting_tests}, 'list of waiting tests expected to be absent' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests to be absent' );
};
subtest 'unknown batch (batch_status)' => sub {
my $unknown_batch = 10;
dies_ok {
$rpcapi->batch_status( { batch_id => $unknown_batch } );
} 'getting results for an unknown batch_id should die';
my $res = $@;
is( $res->{error}, 'Zonemaster::Backend::Error::ResourceNotFound', 'correct error type' );
is( $res->{message}, 'Unknown batch', 'correct error message' );
is( $res->{data}->{batch_id}, $unknown_batch, 'correct data type returned' );
};
};
subtest 'batch with several domains' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = sort( 'afnic.fr', 'iis.se' );
my $res = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $res, 1, 'correct batch job id returned' );
# No lists of test IDs requested
$res = $rpcapi->batch_status( { batch_id => 1 } );
is( $res->{waiting_count}, scalar @domains, 'correct number of running tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
ok( !exists $res->{waiting_tests}, 'list of waiting tests expected to be absent' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests expected to be absent' );
# List of waiting test IDs requested
$res = $rpcapi->batch_status( { batch_id => 1, list_waiting_tests => 1 } );
is( $res->{waiting_count}, scalar @domains, 'correct number of runninng tests' );
is( $res->{running_count}, 0, 'correct number of finished tests' );
is( $res->{finished_count}, 0, 'correct number of finished tests' );
is( scalar @{ $res->{waiting_tests} }, scalar @domains, 'correct number of elements in waiting_tests' );
ok( !exists $res->{running_tests}, 'list of running tests expected to be absent' );
ok( !exists $res->{finished_tests}, 'list of finished tests expected to be absent' );
subtest 'table "test_results" contains 2 entries' => sub {
my ( $count ) = $dbh->selectrow_array( q[ SELECT count(*) FROM test_results ] );
is( $count, @domains, 'two rows in table' );
my $rows = $dbh->selectall_hashref(
q[
SELECT
hash_id,
domain,
batch_id,
created_at,
started_at,
ended_at,
params
FROM test_results
],
'domain'
);
my @keys = sort keys %$rows;
is_deeply( \@keys, \@domains, 'correct domains' );
foreach my $domain ( @keys ) {
is( length($rows->{$domain}->{hash_id}), 16, "[$domain] correct hash_id length" );
is( $rows->{$domain}->{batch_id}, 1, "[$domain] correct batch_id" );
ok( $rows->{$domain}->{created_at}, "[$domain] defined creation time" );
check_tolerance( $rows->{$domain}->{created_at}, "[$domain] creation time in tolerance zone" );
ok( ! defined $rows->{$domain}->{started_at}, "[$domain] undefined start time" );
ok( ! defined $rows->{$domain}->{ended_at}, "[$domain] undefined end time" );
}
};
};
subtest 'batch job still running' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my @domains = ( 'afnic.fr' );
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 1, 'correct batch job id returned' );
subtest 'a batch is already running for the user, new batch creation should not fail' => sub {
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 2, 'same user can create another batch' );
};
subtest 'use another user' => sub {
my $another_user = { username => 'another', api_key => 'token' };
$rpcapi->add_api_user( $another_user );
my $batch_id = $rpcapi->add_batch_job(
{
%$another_user,
domains => \@domains,
test_params => $params
}
);
is( $batch_id, 3, 'another_user can create another batch' );
};
};
subtest 'duplicate user should fail' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
# do not output any error message
my $printerror_before = $rpcapi->{db}->dbh->{PrintError};
$rpcapi->{db}->dbh->{PrintError} = 0;
dies_ok {
$rpcapi->add_api_user( { username => $user->{username}, api_key => "another api key" } );
} 'a user with the same username already exists, add_api_user should die';
my $res = $@;
is( $res->{error}, 'Zonemaster::Backend::Error::Conflict', 'correct error type' );
is( $res->{message}, 'User already exists', 'correct error message' );
is( $res->{data}->{username}, $user->{username}, 'correct data type returned' );
# reset attribute value
$rpcapi->{db}->dbh->{PrintError} = $printerror_before;
};
subtest 'normalize "domain" column' => sub {
my $config = Zonemaster::Backend::Config->parse( $config );
my $rpcapi = init_backend( $config );
my $dbh = $rpcapi->{db}->dbh;
my %domains_to_test = (
"aFnIc.Fr" => "afnic.fr",
"afnic.fr." => "afnic.fr",
"aFnic.Fr." => "afnic.fr"
);
my @domains = keys %domains_to_test;
my $batch_id = $rpcapi->add_batch_job(
{
%$user,
domains => \@domains,
test_params => $params
}
);
my @db_domain = map { $$_[0] } $dbh->selectall_array( "SELECT domain FROM test_results WHERE batch_id=?", undef, $batch_id );
is( @db_domain, 3, '3 tests created' );
my @expected = values %domains_to_test;
is_deeply( \@db_domain, \@expected, 'domains are normalized' );
};
# TODO: create an agent and run batch tests
## Create the agent
#use_ok( 'Zonemaster::Backend::TestAgent' );
#my $agent = Zonemaster::Backend::TestAgent->new( { dbtype => "$db_backend", config => $config } );
#isa_ok($agent, 'Zonemaster::Backend::TestAgent', 'agent');
done_testing();

View File

@@ -0,0 +1,891 @@
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Test::Differences;
use Test::Exception;
use Log::Any::Test; # Must come before use Log::Any
use File::Basename qw( dirname );
use File::Slurp qw( read_file );
use File::Spec::Functions qw( catfile );
use Log::Any qw( $log );
subtest 'Everything but NoWarnings' => sub {
use_ok( 'Zonemaster::Backend::Config' );
subtest 'Set values' => sub {
my $text = q{
[DB]
engine = sqlite
polling_interval = 1.5
[MYSQL]
host = mysql-host
port = 3456
user = mysql_user
password = mysql_password
database = mysql_database
[POSTGRESQL]
host = postgresql-host
port = 6543
user = postgresql_user
password = postgresql_password
database = postgresql_database
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = sv_FI
[PUBLIC PROFILES]
default = /path/to/default.profile
two = /path/to/two.profile
[PRIVATE PROFILES]
three = /path/to/three.profile
four = /path/to/four.profile
[ZONEMASTER]
max_zonemaster_execution_time = 1200
number_of_processes_for_frontend_testing = 30
number_of_processes_for_batch_testing = 40
lock_on_queue = 1
age_reuse_previous_test = 800
};
my $config = Zonemaster::Backend::Config->parse( $text );
isa_ok $config, 'Zonemaster::Backend::Config', 'parse() return value';
is $config->DB_engine, 'SQLite', 'set: DB.engine';
is $config->DB_polling_interval, 1.5, 'set: DB.polling_interval';
is $config->MYSQL_host, 'mysql-host', 'set: MYSQL.host';
is $config->MYSQL_port, 3456, 'set: MYSQL.port';
is $config->MYSQL_user, 'mysql_user', 'set: MYSQL.user';
is $config->MYSQL_password, 'mysql_password', 'set: MYSQL.password';
is $config->MYSQL_database, 'mysql_database', 'set: MYSQL.database';
is $config->POSTGRESQL_host, 'postgresql-host', 'set: POSTGRESQL.host';
is $config->POSTGRESQL_port, 6543, 'set: POSTGRESQL.port';
is $config->POSTGRESQL_user, 'postgresql_user', 'set: POSTGRESQL.user';
is $config->POSTGRESQL_password, 'postgresql_password', 'set: POSTGRESQL.password';
is $config->POSTGRESQL_database, 'postgresql_database', 'set: POSTGRESQL.database';
is $config->SQLITE_database_file, '/var/db/zonemaster.sqlite', 'set: SQLITE.database_file';
eq_or_diff { $config->LANGUAGE_locale }, { sv => 'sv_FI' }, 'set: LANGUAGE.locale';
eq_or_diff { $config->PUBLIC_PROFILES }, { #
default => '/path/to/default.profile',
two => '/path/to/two.profile'
},
'set: PUBLIC PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, { #
three => '/path/to/three.profile',
four => '/path/to/four.profile'
},
'set: PRIVATE PROFILES';
is $config->ZONEMASTER_max_zonemaster_execution_time, 1200, 'set: ZONEMASTER.max_zonemaster_execution_time';
is $config->ZONEMASTER_number_of_processes_for_frontend_testing, 30, 'set: ZONEMASTER.number_of_processes_for_frontend_testing';
is $config->ZONEMASTER_number_of_processes_for_batch_testing, 40, 'set: ZONEMASTER.number_of_processes_for_batch_testing';
is $config->ZONEMASTER_lock_on_queue, 1, 'set: ZONEMASTER.lock_on_queue';
is $config->ZONEMASTER_age_reuse_previous_test, 800, 'set: ZONEMASTER.age_reuse_previous_test';
};
subtest 'Default values' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
cmp_ok abs( $config->DB_polling_interval - 0.5 ), '<', 0.000001, 'default: DB.polling_interval';
is $config->MYSQL_port, 3306, 'default: MYSQL.port';
is $config->POSTGRESQL_port, 5432, 'default: POSTGRESQL.port';
eq_or_diff { $config->LANGUAGE_locale }, { en => 'en_US' }, 'default: LANGUAGE.locale';
eq_or_diff { $config->PUBLIC_PROFILES }, { default => undef }, 'default: PUBLIC_PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, {}, 'default: PRIVATE_PROFILES';
is $config->ZONEMASTER_max_zonemaster_execution_time, 600, 'default: ZONEMASTER.max_zonemaster_execution_time';
is $config->ZONEMASTER_number_of_processes_for_frontend_testing, 20, 'default: ZONEMASTER.number_of_processes_for_frontend_testing';
is $config->ZONEMASTER_number_of_processes_for_batch_testing, 20, 'default: ZONEMASTER.number_of_processes_for_batch_testing';
is $config->ZONEMASTER_lock_on_queue, 0, 'default: ZONEMASTER.lock_on_queue';
is $config->ZONEMASTER_age_reuse_previous_test, 600, 'default: ZONEMASTER.age_reuse_previous_test';
is $config->RPCAPI_enable_add_api_user, 0, 'default: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 1, 'default: RPCAPI.enable_add_batch_job';
};
SKIP: {
skip "no more deprecated values", 1;
subtest 'Deprecated values and fallbacks that are unconditional' => sub {
$log->clear();
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
};
}
subtest 'Warnings' => sub {
$log->clear();
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = localhost
port = 3333
user = mysql_user
password = mysql_password
database = mysql_database
};
my $config = Zonemaster::Backend::Config->parse( $text );
$log->contains_ok( qr/MYSQL\.port.*MYSQL\.host/, 'warning: MYSQL.host is "localhost" and MYSQL.port defined' );
is $config->MYSQL_host, 'localhost', 'set: MYSQL.host';
is $config->MYSQL_port, 3333, 'set: MYSQL.port';
};
throws_ok {
$log->clear();
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale =
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Use of empty LANGUAGE.locale property is not permitted/, 'die: Invalid empty locale tag';
throws_ok {
my $text = '{"this":"is","not":"a","valid":"ini","file":"!"}';
Zonemaster::Backend::Config->parse( $text );
}
qr/Failed to parse config/, 'die: Invalid INI format';
throws_ok {
my $text = q{
[DB]
engine = Excel
[SQLITE]
databse_file = /var/db/zonemaster.sqlite
[ZNMEOTAESR]
lock_on_queue = 1
};
Zonemaster::Backend::Config->parse( $text );
}
qr{section.*ZNMEOTAESR}, 'die: Invalid section name';
throws_ok {
my $text = q{
[DB]
engine = SQLite
pnlilog_iatnvrel = 0.5
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{property.*pnlilog_iatnvrel}, 'die: Invalid property name';
throws_ok {
my $text = q{
[DB]
engine = Excel
};
Zonemaster::Backend::Config->parse( $text );
}
qr/DB\.engine.*Excel/, 'die: Invalid DB.engine value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
polling_interval = hourly
[SQLITE]
databse_file = /var/db/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{DB\.polling_interval.*hourly}, 'die: Invalid DB.polling_interval value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = 192.0.2.1:3306
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.host.*192.0.2.1:3306}, 'die: Invalid MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = Robert'); DROP TABLE Students;--
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.user.*Robert'\); DROP TABLE Students;--}, 'die: Invalid MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster
password = (╯°□°)╯︵ ┻━┻
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.password.*\(╯°□°\)╯︵ ┻━┻}, 'die: Invalid MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
database = |)/-\'|'/-\|3/-\$[-
};
Zonemaster::Backend::Config->parse( $text );
}
qr{MYSQL\.database.*|\)/-\'|'/-\\|3/-\\$[-}, 'die: Invalid MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = 192.0.2.1:5432
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.host.*192.0.2.1:5432}, 'die: Invalid POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = Robert'); DROP TABLE Students;--
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.user.*Robert'\); DROP TABLE Students;--}, 'die: Invalid POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster
password = (╯°□°)╯︵ ┻━┻
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.password.*\(╯°□°\)╯︵ ┻━┻}, 'die: Invalid POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
database = |)/-\'|'/-\|3/-\$[-
};
Zonemaster::Backend::Config->parse( $text );
}
qr{POSTGRESQL\.database.*|\)/-\'|'/-\\|3/-\\$[-}, 'die: Invalid POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = ./relative/path/to/zonemaster.sqlite
};
Zonemaster::Backend::Config->parse( $text );
}
qr{SQLITE\.database_file.*\./relative/path/to/zonemaster.sqlite}, 'die: Invalid SQLITE.database_file value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
max_zonemaster_execution_time = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.max_zonemaster_execution_time.*0}, 'die: Invalid ZONEMASTER.max_zonemaster_execution_time value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
lock_on_queue = -1
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.lock_on_queue.*-1}, 'die: Invalid ZONEMASTER.lock_on_queue value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
number_of_processes_for_frontend_testing = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.number_of_processes_for_frontend_testing.*0}, 'die: Invalid ZONEMASTER.number_of_processes_for_frontend_testing value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
number_of_processes_for_batch_testing = 100000
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.number_of_processes_for_batch_testing.*100000}, 'die: Invalid ZONEMASTER.number_of_processes_for_batch_testing value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[ZONEMASTER]
age_reuse_previous_test = 0
};
Zonemaster::Backend::Config->parse( $text );
}
qr{ZONEMASTER\.age_reuse_previous_test.*0}, 'die: Invalid ZONEMASTER.age_reuse_previous_test value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.host/, 'die: Missing MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.user/, 'die: Missing MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.password/, 'die: Missing MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.database/, 'die: Missing MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.host/, 'die: Missing POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.user/, 'die: Missing POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.password/, 'die: Missing POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.database/, 'die: Missing POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.host/, 'die: Missing MYSQL.host value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.user/, 'die: Missing MYSQL.user value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.password/, 'die: Missing MYSQL.password value';
throws_ok {
my $text = q{
[DB]
engine = MySQL
[MYSQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/MYSQL\.database/, 'die: Missing MYSQL.database value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
user = zonemaster_user
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.host/, 'die: Missing POSTGRESQL.host value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
password = zonemaster_password
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.user/, 'die: Missing POSTGRESQL.user value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
database = zonemaster_database
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.password/, 'die: Missing POSTGRESQL.password value';
throws_ok {
my $text = q{
[DB]
engine = PostgreSQL
[POSTGRESQL]
host = zonemaster-host
user = zonemaster_user
password = zonemaster_password
};
Zonemaster::Backend::Config->parse( $text );
}
qr/POSTGRESQL\.database/, 'die: Missing POSTGRESQL.database value';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = English
};
Zonemaster::Backend::Config->parse( $text );
}
qr/LANGUAGE\.locale.*English/, 'die: Invalid locale_tag in LANGUAGE.locale';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[LANGUAGE]
locale = en_GB en_US
};
Zonemaster::Backend::Config->parse( $text );
}
qr/LANGUAGE\.locale.*en/, 'die: Repeated language code in LANGUAGE.locale';
lives_and {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
DEFAULT = /path/to/my.profile
[PRIVATE PROFILES]
SECRET = /path/to/my.profile
};
my $config = Zonemaster::Backend::Config->parse( $text );
eq_or_diff { $config->PUBLIC_PROFILES }, { default => '/path/to/my.profile' }, 'normalize profile names under PUBLIC PROFILES';
eq_or_diff { $config->PRIVATE_PROFILES }, { secret => '/path/to/my.profile' }, 'normalize profile names under PRIVATE PROFILES';
};
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
-invalid-name- = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PUBLIC PROFILES.*-invalid-name-/, 'die: Invalid profile name in PUBLIC PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
-invalid-name- = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PRIVATE PROFILES.*-invalid-name-/, 'die: Invalid profile name in PRIVATE PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
valid-name = relative/path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/absolute.*valid-name/, 'die: Invalid absolute path in PUBLIC PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
valid-name = relative/path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/absolute.*valid-name/, 'die: Invalid absolute path in PRIVATE PROFILES';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
valid-name = /path/to/my.profile
valid-name = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*valid-name/, 'die: Repeated profile name in PUBLIC PROFILES section';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
valid-name = /path/to/my.profile
valid-name = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*valid-name/, 'die: Repeated profile name in PRIVATE PROFILES section';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PUBLIC PROFILES]
pub-and-priv = /path/to/my.profile
[PRIVATE PROFILES]
pub-and-priv = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/unique.*pub-and-priv/, 'die: Repeated profile name across sections';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[PRIVATE PROFILES]
default = /path/to/my.profile
};
Zonemaster::Backend::Config->parse( $text );
}
qr/PRIVATE PROFILES.*default/, 'die: Default profile in PRIVATE PROFILES';
subtest 'RPCAPI experimental aliases' => sub {
subtest 'default values' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
};
my $config = Zonemaster::Backend::Config->parse( $text );
is $config->RPCAPI_enable_add_api_user, 0, 'default: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 1, 'default: RPCAPI.enable_add_batch_job';
is $config->RPCAPI_enable_user_create, 0, 'default: RPCAPI.enable_user_create';
is $config->RPCAPI_enable_batch_create, 1, 'default: RPCAPI.enable_batch_create';
};
subtest 'specifying stable and experimental parameters is forbidden' => sub {
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_user_create = no
enable_add_api_user = yes
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Error:.+RPCAPI\.enable_add_api_user.+RPCAPI\.enable_user_create/, 'die: RPCAPI stable and experimental alias (add_api_user/user_create)';
throws_ok {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_add_batch_job = no
enable_batch_create = no
};
Zonemaster::Backend::Config->parse( $text );
}
qr/Error:.+RPCAPI\.enable_add_batch_job.+RPCAPI\.enable_batch_create/, 'die: RPCAPI stable and experimental alias (batch_job/batch_create)';
};
subtest 'setting alias' => sub {
my $text = q{
[DB]
engine = SQLite
[SQLITE]
database_file = /var/db/zonemaster.sqlite
[RPCAPI]
enable_user_create = no
enable_batch_create = no
};
my $config = Zonemaster::Backend::Config->parse( $text );
is $config->RPCAPI_enable_user_create, 0, 'set: RPCAPI.enable_user_create';
is $config->RPCAPI_enable_batch_create, 0, 'set: RPCAPI.enable_batch_create';
is $config->RPCAPI_enable_add_api_user, 0, 'aliased: RPCAPI.enable_add_api_user';
is $config->RPCAPI_enable_add_batch_job, 0, 'aliased: RPCAPI.enable_add_batch_job';
};
};
{
my $path = catfile( dirname( $0 ), '..', 'share', 'backend_config.ini' );
my $text = read_file( $path );
lives_ok {
Zonemaster::Backend::Config->parse( $text );
} 'default config is valid';
}
};

221
zonemaster-backend/t/db.t Normal file
View File

@@ -0,0 +1,221 @@
use strict;
use warnings;
use utf8;
use Encode;
use Test::More; # see done_testing()
use_ok( 'Zonemaster::Backend::DB' );
sub encode_and_fingerprint {
my $params = shift;
my $self = "Zonemaster::Backend::DB";
my $encoded_params = $self->encode_params( $params );
my $fingerprint = $self->generate_fingerprint( $params );
return ( $encoded_params, $fingerprint );
}
subtest 'encoding and fingerprint' => sub {
subtest 'missing properties' => sub {
my %params = ( domain => "example.com" );
my $expected_encoded_params = '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}';
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'domain only: the encoded strings should match';
#diag ($fingerprint);
my $expected_encoded_params_v4_true = '{"domain":"example.com","ds_info":[],"ipv4":true,"ipv6":null,"nameservers":[],"profile":"default"}';
$params{ipv4} = JSON::PP->true;
my ( $encoded_params_ipv4, $fingerprint_ipv4 ) = encode_and_fingerprint( \%params );
is $encoded_params_ipv4, $expected_encoded_params_v4_true, 'add ipv4: the encoded strings should match';
isnt $fingerprint_ipv4, $fingerprint, 'fingerprints should not match';
};
subtest 'array properties' => sub {
subtest 'ds_info' => sub {
my %params1 = (
domain => "example.com",
ds_info => [{
algorithm => 8,
keytag => 11627,
digtype => 2,
digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448"
}]
);
my %params2 = (
ds_info => [{
digtype => 2,
algorithm => 8,
keytag => 11627,
digest => "a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448"
}],
domain => "example.com"
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'ds_info same fingerprint';
is $encoded_params1, $encoded_params2, 'ds_info same encoded string';
};
subtest 'nameservers order' => sub {
my %params1 = (
domain => "example.com",
nameservers => [
{ ns => "ns2.nic.fr", ip => "192.134.4.1" },
{ ns => "ns1.nic.fr" },
{ ip => "192.0.2.1", ns => "ns3.nic.fr"}
]
);
my %params2 = (
nameservers => [
{ ns => "ns3.nic.fr", ip => "192.0.2.1" },
{ ns => "ns1.nic.fr" },
{ ip => "192.134.4.1", ns => "ns2.nic.fr"}
],
domain => "example.com"
);
my %params3 = (
domain => "example.com",
nameservers => [
{ ip => "", ns => "ns1.nic.fr" },
{ ns => "ns3.nic.FR", ip => "192.0.2.1" },
{ ns => "ns2.nic.fr", ip => "192.134.4.1" }
]
);
my %params4 = (
domain => "example.com",
nameservers => [
{ ip => "192.134.4.1", ns => "nS2.Nic.FR"},
{ ns => "Ns1.nIC.fR", ip => "" },
{ ns => "ns3.nic.fr", ip => "192.0.2.1" }
]
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
my ( $encoded_params3, $fingerprint3 ) = encode_and_fingerprint( \%params3 );
my ( $encoded_params4, $fingerprint4 ) = encode_and_fingerprint( \%params4 );
is $fingerprint1, $fingerprint2, 'nameservers: same fingerprint';
is $encoded_params1, $encoded_params2, 'nameservers: same encoded string';
is $fingerprint1, $fingerprint3, 'nameservers: same fingerprint (empty ip)';
is $encoded_params1, $encoded_params3, 'nameservers: same encoded string (empty ip)';
is $fingerprint1, $fingerprint4, 'nameservers: same fingerprint (ignore nameservers\' ns case)';
is $encoded_params1, $encoded_params4, 'nameservers: same encoded string (ignore nameservers\' ns case)';
};
};
subtest 'should be case insensitive' => sub {
my %params1 = ( domain => "example.com" );
my %params2 = ( domain => "eXamPLe.COm" );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $encoded_params2, 'same encoded string';
};
subtest 'garbage properties set' => sub {
my $expected_encoded_params = '{"client":"GUI v3.3.0","domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}';
my %params1 = (
domain => "example.com",
);
my %params2 = (
domain => "example.com",
client => "GUI v3.3.0"
);
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'leave out garbage property in fingerprint computation...';
is $encoded_params2, $expected_encoded_params, '...but keep it in the encoded string';
};
subtest 'should have different fingerprints' => sub {
subtest 'different profiles' => sub {
my %params1 = (
domain => "example.com",
profile => "profile_1"
);
my %params2 = (
domain => "example.com",
profile => "profile_2"
);
my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
isnt $fingerprint1, $fingerprint2, 'different profiles, different fingerprints';
};
subtest 'different IP protocols' => sub {
my %params1 = (
domain => "example.com",
ipv4 => "true",
ipv6 => "false"
);
my %params2 = (
domain => "example.com",
ipv4 => "false",
ipv6 => "true"
);
my ( undef, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( undef, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
isnt $fingerprint1, $fingerprint2, 'different IP protocols, different fingerprints';
};
};
subtest 'IDN domain' => sub {
my $expected_encoded_params = encode_utf8( '{"domain":"xn--caf-dma.example","ds_info":[],"ipv4":true,"ipv6":true,"nameservers":[],"profile":"default"}' );
my $expected_fingerprint = '8cb027ff2c175f48aed2623abad0cdd2';
my %params = ( domain => "café.example" );
$params{ipv4} = JSON::PP->true;
$params{ipv6} = JSON::PP->true;
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'IDN domain: the encoded strings should match';
is $fingerprint, $expected_fingerprint, 'IDN domain: correct fingerprint';
};
subtest 'final dots' => sub {
subtest 'in domain' => sub {
my %params1 = ( domain => "example.com" );
my %params2 = ( domain => "example.com." );
my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $expected_encoded_params, 'the encoded strings should match';
};
subtest 'in nameserver' => sub {
my %params1 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com." } ] );
my %params2 = ( domain => "example.com", nameservers => [ { ns => "ns1.example.com" } ] );
my $expected_encoded_params = encode_utf8( '{"domain":"example.com","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[{"ns":"ns1.example.com"}],"profile":"default"}' );
my ( $encoded_params1, $fingerprint1 ) = encode_and_fingerprint( \%params1 );
my ( $encoded_params2, $fingerprint2 ) = encode_and_fingerprint( \%params2 );
is $fingerprint1, $fingerprint2, 'same fingerprint';
is $encoded_params1, $expected_encoded_params, 'the encoded strings should match';
};
subtest 'root is not modified' => sub {
my %params = ( domain => "." );
my $expected_encoded_params = encode_utf8( '{"domain":".","ds_info":[],"ipv4":null,"ipv6":null,"nameservers":[],"profile":"default"}' );
my ( $encoded_params, $fingerprint ) = encode_and_fingerprint( \%params );
is $encoded_params, $expected_encoded_params, 'the encoded strings should match';
};
};
};
done_testing();

View File

@@ -0,0 +1,150 @@
use strict;
use warnings;
use Test::More tests => 2;
use Test::Exception;
use Test::NoWarnings qw(warnings clear_warnings);
use File::ShareDir qw[dist_file];
use File::Temp qw[tempdir];
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
my $dbclass = Zonemaster::Backend::DB->get_db_class( $db_backend );
my $db = $dbclass->from_config( $config );
subtest 'Everything but Test::NoWarnings' => sub {
subtest 'drop and create' => sub {
subtest 'first drop (cleanup) ... ' => sub {
$db->drop_tables();
dies_ok {
$db->dbh->do( 'SELECT 1 FROM test_results' )
}
'table "test_results" sould not exist';
};
subtest '... then drop after create ...' => sub {
$db->create_schema();
my ( $res ) = $db->dbh->selectrow_array( 'SELECT count(*) FROM test_results' );
is $res, 0, 'a. after create, table "test_results" should exist and be empty';
$db->drop_tables();
dies_ok {
$db->dbh->do( 'SELECT 1 FROM test_results' )
}
'b. after drop, table "test_results" sould be removed';
};
};
subtest 'constraints' => sub {
$db->create_schema();
subtest 'constraint unique' => sub {
my $time = $db->format_time( time() );
my @constraints = (
{
table => 'test_results',
key => 'hash_id',
sql => "INSERT INTO test_results (hash_id,domain,created_at,params)
VALUES ('0123456789abcdef', 'domain.test', '$time', '{}')"
},
{
table => 'log_level',
key => 'level',
sql => "INSERT INTO log_level (level, value) VALUES ('OTHER', 10)"
},
{
table => 'users',
key => 'username',
sql => "INSERT INTO users (username) VALUES ('user1')"
},
);
for my $c (@constraints) {
$db->dbh->do( $c->{sql} );
throws_ok {
$db->dbh->do( $c->{sql} );
}
qr/(unique constraint|duplicate entry)/i, "$c->{table}($c->{key}) key should be unique";
}
};
subtest 'constraint on foreign key' => sub {
subtest 'result_entries - hash_id should exist in test_results(hash_id)' => sub {
my $hash_id_ok = "0123456789abcdef";
# INFO is 1
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('$hash_id_ok', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
my $inserted_rows = $db->dbh->do( $sql );
is $inserted_rows, 1, 'can insert an entry with an existing hash_id';
throws_ok {
my $hash_id_ko = "aaaaaaaaaaaaaaaa";
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('$hash_id_ko', 1, 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
$db->dbh->do( $sql );
}
qr/foreign key/i, 'cannot insert an entry with an non-existing hash_id';
};
subtest 'result_entries - level should exist in log_level(level)' => sub {
my $level = 1; # INFO
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
my $inserted_rows = $db->dbh->do( $sql );
is $inserted_rows, 1, 'can insert an entry with an existing level';
throws_ok {
my $level = 42; # does not exist
my $sql = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args)
VALUES ('0123456789abcdef', '$level', 'MODULE', 'TESTCASE', 'TAG', 42, '{}')";
$db->dbh->do( $sql );
}
qr/foreign key/i, 'cannot insert an entry with an non-existing level';
};
};
};
};
# FIXME: hack to avoid getting warnings from Test::NoWarnings
my @warn = warnings();
if ( @warn == 7 ) {
clear_warnings();
}

View File

@@ -0,0 +1,26 @@
i.root-servers.net 192.36.148.17 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"192.36.148.17","querytime":10,"timestamp":1646935543.48935,"data":"+U2EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
i.root-servers.net 2001:07fe:0000:0000:0000:0000:0000:0053 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"zQCEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.51238,"answerfrom":"2001:7fe::53","querytime":9}}}}
d.root-servers.net 199.7.91.13 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.15536,"data":"QraEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":14,"answerfrom":"199.7.91.13"}}}}
d.root-servers.net 2001:0500:002d:0000:0000:0000:0000:000d {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":10,"answerfrom":"2001:500:2d::d","timestamp":1646935543.18271,"data":"nQOEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
c.root-servers.net 2001:0500:0002:0000:0000:0000:0000:000c {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"7VmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.1185,"answerfrom":"2001:500:2::c","querytime":23}}}}
c.root-servers.net 192.33.4.12 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":24,"answerfrom":"192.33.4.12","timestamp":1646935543.08159,"data":"+EmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
j.root-servers.net 2001:0503:0c27:0000:0000:0000:0002:0030 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.54733,"data":"bdyEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"2001:503:c27::2:30","querytime":10}}}}
j.root-servers.net 192.58.128.30 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.53484,"data":"8oWEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"192.58.128.30","querytime":2}}}}
b.root-servers.net 2001:0500:0200:0000:0000:0000:0000:000b {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"2001:500:200::b","querytime":13,"data":"rtKEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.05494}}}}
b.root-servers.net 199.9.14.201 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"eSqEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.02769,"querytime":14,"answerfrom":"199.9.14.201"}}}}
l.root-servers.net 2001:0500:009f:0000:0000:0000:0000:0042 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"3tWEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.63711,"answerfrom":"2001:500:9f::42","querytime":10}}}}
l.root-servers.net 199.7.83.42 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.61372,"data":"LXeEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":10,"answerfrom":"199.7.83.42"}}}}
m.root-servers.net 2001:0dc3:0000:0000:0000:0000:0000:0035 {"WcLt/i2sUcZA//eE56F52g":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"2001:dc3::35","timestamp":1646935543.86015,"data":"LDiEAwABAAAAAQAAB2V4YW1wbGUAAAYAAQAABgABAAAAAABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}},"5e42wXPdot60bOvWyxbkKQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.85123,"data":"ysiEAwABAAAAAQAAD3huLS1hbnRoci12cmE3agdleGFtcGxlAAABAAEAAAYAAQABUYAAQAFhDHJvb3Qtc2VydmVycwNuZXQABW5zdGxkDHZlcmlzaWduLWdycwNjb20AeIW+mQAABwgAAAOEAAk6gAABUYA=","querytime":3,"answerfrom":"2001:dc3::35"}}},"Su5WLHq4snuuB/mBxXyF0Q":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935542.94235,"data":"mhKEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAYAAQAABgABAAAAAABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":3,"answerfrom":"2001:dc3::35"}}},"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"2001:dc3::35","data":"k96EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.66816}}}}
m.root-servers.net 202.12.27.33 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"202.12.27.33","timestamp":1646935543.65873,"data":"yH6EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
k.root-servers.net 2001:07fd:0000:0000:0000:0000:0000:0001 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":8,"answerfrom":"2001:7fd::1","data":"oQmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.59331}}}}
k.root-servers.net 193.0.14.129 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":12,"answerfrom":"193.0.14.129","data":"pCmEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.56745}}}}
g.root-servers.net 2001:0500:0012:0000:0000:0000:0000:0d0d {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":55,"answerfrom":"2001:500:12::d0d","data":"NNGEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.35495}}}}
g.root-servers.net 192.112.36.4 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.26682,"data":"6cSEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":75,"answerfrom":"192.112.36.4"}}}}
a.root-servers.net 198.41.0.4 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935542.98134,"data":"ZYGEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","answerfrom":"198.41.0.4","querytime":10}}}}
a.root-servers.net 2001:0503:ba3e:0000:0000:0000:0002:0030 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"16WEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.00459,"querytime":10,"answerfrom":"2001:503:ba3e::2:30"}}}}
f.root-servers.net 2001:0500:002f:0000:0000:0000:0000:000f {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"P+2EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.25261,"answerfrom":"2001:500:2f::f","querytime":4}}}}
f.root-servers.net 192.5.5.241 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"timestamp":1646935543.23842,"data":"VuuEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","querytime":3,"answerfrom":"192.5.5.241"}}}}
h.root-servers.net 2001:0500:0001:0000:0000:0000:0000:0053 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"answerfrom":"2001:500:1::53","querytime":9,"timestamp":1646935543.46742,"data":"NyuEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
h.root-servers.net 198.97.190.53 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"zsqEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.4237,"answerfrom":"198.97.190.53","querytime":31}}}}
e.root-servers.net 192.203.230.10 {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"querytime":3,"answerfrom":"192.203.230.10","timestamp":1646935543.20502,"data":"C8CEAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA=="}}}}
e.root-servers.net 2001:0500:00a8:0000:0000:0000:0000:000e {"OfMmGFE/IOgA39Hya8P8tQ":{"Zonemaster::Engine::Packet":{"Zonemaster::LDNS::Packet":{"data":"O0+EAwABAAAAAQAAC3huLS1jYWYtZG1hB2V4YW1wbGUAAAIAAQAABgABAAFRgABAAWEMcm9vdC1zZXJ2ZXJzA25ldAAFbnN0bGQMdmVyaXNpZ24tZ3JzA2NvbQB4hb6ZAAAHCAAAA4QACTqAAAFRgA==","timestamp":1646935543.21714,"querytime":9,"answerfrom":"2001:500:a8::e"}}}}

126
zonemaster-backend/t/idn.t Normal file
View File

@@ -0,0 +1,126 @@
use strict;
use warnings;
use 5.14.2;
use Data::Dumper;
use File::Temp qw[tempdir];
use Test::Exception;
use Test::More; # see done_testing()
use utf8;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( TestAgent );
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $datafile = "$t_path/idn.data";
TestUtil::restore_datafile( $datafile );
my $tempdir = tempdir( CLEANUP => 1 );
my $configuration = <<"EOF";
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
EOF
if ( $ENV{ZONEMASTER_RECORD} ) {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_network_true.json
default=$t_path/test_profile_network_true.json
EOF
} else {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_no_network.json
default=$t_path/test_profile_no_network.json
EOF
}
my $config = Zonemaster::Backend::Config->parse( $configuration );
my $db = TestUtil::init_db( $config );
my $agent = TestUtil::create_testagent( $config );
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
domain => 'café.example',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'default',
};
my $test_id;
subtest 'test IDN domain' => sub {
$test_id = $db->create_new_test( $params->{domain}, $params, 10 );
my $res = $db->get_test_params( $test_id );
note Dumper($res);
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
};
# run the test
$db->claim_test( $test_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
$agent->run( $test_id ); # blocking call
subtest 'test get_test_results' => sub {
my $res = $db->test_results( $test_id );
is( $res->{params}->{domain}, $params->{domain}, 'Retrieve the correct domain name' );
};
subtest 'test IDN nameserver' => sub {
$params->{nameservers} = [ { ns => "anøthær.example" } ];
$test_id = $db->create_new_test( $params->{domain}, $params, 10 );
subtest 'get_test_params' => sub {
my $res = $db->get_test_params( $test_id );
note Dumper($res);
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
};
# run the test
$db->claim_test( $test_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
$agent->run( $test_id ); # blocking call
subtest 'test_results' => sub {
my $res = $db->test_results( $test_id );
is_deeply( $res->{params}->{nameservers}, $params->{nameservers}, 'Retrieve the correct nameservers parameters' );
};
};
TestUtil::save_datafile( $datafile );
done_testing();

View File

@@ -0,0 +1,301 @@
use strict;
use warnings;
use 5.14.2;
use Test::More tests => 2;
use Test::Exception;
use Test::NoWarnings;
use Log::Any::Test;
use Log::Any qw( $log );
my $TIME;
BEGIN {
$TIME = CORE::time();
*CORE::GLOBAL::time = sub { $TIME };
}
use Data::Dumper;
use File::ShareDir qw[dist_file];
use File::Temp qw[tempdir];
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB qw( $TEST_WAITING $TEST_RUNNING $TEST_COMPLETED );
sub advance_time {
my ( $delta ) = @_;
$TIME += $delta;
}
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
sub count_cancellation_messages {
my $results = shift;
return scalar grep { $_->{tag} eq 'UNABLE_TO_FINISH_TEST' } @{ $results->{results} };
}
sub count_died_messages {
my $results = shift;
return scalar grep { $_->{tag} eq 'TEST_DIED' } @{ $results->{results} };
}
subtest 'Everything but Test::NoWarnings' => sub {
lives_ok { # Make sure we get to print log messages in case of errors.
my $db = TestUtil::init_db( $config );
subtest 'State transitions' => sub {
my $testid1 = $db->create_new_test( "1.transition.test", {}, 10 );
is ref $testid1, '', "create_new_test should return 'testid' scalar";
my $current_state = $db->test_state( $testid1 );
is $current_state, $TEST_WAITING, "New test starts out in 'waiting' state.";
my @cases = (
{
old_state => $TEST_WAITING,
transition => [ 'store_results', '{}' ],
throws => qr/illegal transition/,
},
{
old_state => $TEST_WAITING,
transition => ['claim_test'],
returns => 1, # true
new_state => $TEST_RUNNING,
},
{
old_state => $TEST_RUNNING,
transition => ['claim_test'],
returns => '', # false
},
{
old_state => $TEST_RUNNING,
transition => [ 'store_results', '{}' ],
returns => undef,
new_state => $TEST_COMPLETED,
},
{
old_state => $TEST_COMPLETED,
transition => ['claim_test'],
returns => '', #false
},
{
old_state => $TEST_COMPLETED,
transition => [ 'store_results', '{}' ],
throws => qr/illegal transition/,
},
);
for my $case ( @cases ) {
if ( $case->{old_state} ne $current_state ) {
BAIL_OUT( "Assuming to be in '$case->{old_state}' but we're actually in '$current_state'!" );
}
my ( $transition, @args ) = @{ $case->{transition} };
if ( exists $case->{returns} ) {
my $rv_string = Data::Dumper->new( [ $case->{returns} ] )->Indent( 0 )->Terse( 1 )->Dump;
my $result = $db->$transition( $testid1, @args );
is $result,
$case->{returns},
"In state '$case->{old_state}' transition '$transition' should return $rv_string,";
if ( $case->{new_state} ) {
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{new_state},
"and it should move the test to '$case->{new_state}' state.";
}
else {
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{old_state},
"and it should not affect the actual state.";
}
}
elsif ( exists $case->{throws} ) {
throws_ok {
$db->$transition( $testid1, @args )
}
$case->{throws}, "In state '$case->{old_state}' transition '$transition' should throw an exception,";
$current_state = $db->test_state( $testid1 );
is $current_state,
$case->{old_state},
"and it should not affect the actual state.";
}
else {
BAIL_OUT( "Invalid case specification!" );
}
}
};
subtest 'Progress' => sub {
my $testid1 = $db->create_new_test( "1.progress.test", {}, 10 );
is ref $testid1, '', "create_new_test should return 'testid' scalar";
throws_ok { $db->test_progress( $testid1, 1 ) } qr/illegal update/, "Setting progress should throw an exception in 'waiting' state.";
$db->claim_test( $testid1 );
# Logically progress is 0 entering the 'running' state, but because
# of implementation details we're clamping it to the range 1-99
# inclusive.
is $db->test_progress( $testid1 ), 1, "Progress should be 1 entering the 'running' state.";
is $db->test_progress( $testid1, 0 ), 1, "Setting progress to 0 should succeed, but actual clamped value is returned,";
is $db->test_progress( $testid1 ), 1, "and it should persist at the clamped value.";
is $db->test_progress( $testid1, 0 ), 1, "Setting the same progress again should succeed.";
is $db->test_progress( $testid1, 2 ), 2, "Setting a higher progress should be allowed,";
is $db->test_progress( $testid1 ), 2, "and it should persist at the new value.";
is $db->test_progress( $testid1, 2 ), 2, "Setting the same progress again should succeed.";
throws_ok { $db->test_progress( $testid1, 0 ) } qr/illegal update/, "Setting a lower progress should throw an exception,";
is $db->test_progress( $testid1 ), 2, "and it should persist at the old value.";
is $db->test_progress( $testid1, 100 ), 99, "Setting progress to 100 should succeed, but actual clamped value is returned,";
is $db->test_progress( $testid1 ), 99, "and it should persist at the clamped value.";
$db->store_results( $testid1, '{}' );
throws_ok { $db->test_progress( $testid1, 100 ) } qr/illegal update/, "Setting progress should throw an exception in 'completed' state.";
};
subtest 'Testid reuse' => sub {
my $testid1 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is ref $testid1, '', 'create_new_test returns "testid" scalar';
advance_time( 11 );
my $testid2 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is $testid2, $testid1, 'reuse is determined from start time (as opposed to creation time)';
$db->claim_test( $testid1 );
advance_time( 10 );
my $testid3 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
is $testid3, $testid1, 'old testid is reused before it expires';
advance_time( 1 );
my $testid4 = $db->create_new_test( "zone1.rpcapi.example", {}, 10 );
isnt $testid4, $testid1, 'a new testid is generated after the old one expires';
};
subtest 'Termination of timed out tests' => sub {
my $testid2 = $db->create_new_test( "zone2.rpcapi.example", {}, 10 );
my $testid3 = $db->create_new_test( "zone3.rpcapi.example", {}, 10 );
# testid2 started 11 seconds ago, testid3 started 10 seconds ago
$db->claim_test( $testid2 );
advance_time( 1 );
$db->claim_test( $testid3 );
advance_time( 10 );
$db->process_unfinished_tests( undef, 10 );
is $db->test_progress( $testid3 ), 1, 'leave test alone AT its timeout';
is $db->test_progress( $testid2 ), 100, 'terminate test AFTER its timeout';
is count_cancellation_messages( $db->test_results( $testid3 ) ), 0, 'no cancellation message present AT timeout';
is count_cancellation_messages( $db->test_results( $testid2 ) ), 1, 'one cancellation message present AFTER timeout';
};
subtest 'Termination of crashed tests' => sub {
my $testid4 = $db->create_new_test( "zone4.rpcapi.example", {}, 10 );
$db->claim_test( $testid4 );
$db->process_dead_test( $testid4 );
is $db->test_progress( $testid4 ), 100, 'terminates test';
is count_died_messages( $db->test_results( $testid4 ) ), 1, 'one died message present after crash';
};
subtest 'Do not reuse batch tests' => sub {
my %user = (
username => "user",
api_key => "key"
);
my @domains = ( 'zone1.rpcapi.example', 'zone5.rpcapi.example' );
my $params = {
%user,
domains => \@domains,
test_params => {
priority => 5,
queue => 0
}
};
$db->add_api_user( $user{username}, $user{api_key} );
my $batch_id = $db->add_batch_job( $params );
my @batch_test_ids = $db->dbh->selectall_array(
q[
SELECT hash_id
FROM test_results
WHERE batch_id = ?
],
undef,
$batch_id
);
@batch_test_ids = map { $$_[0] } @batch_test_ids;
if ( @batch_test_ids != 2 ) {
BAIL_OUT( 'There should be 2 tests in database for this batch_id' );
}
my ( $count_zone1 ) = $db->dbh->selectrow_array(
q[
SELECT count(*)
FROM test_results
WHERE domain = 'zone1.rpcapi.example'
]
);
is( $count_zone1, 3, '3 tests for domain "zone1.rpcapi.example' );
my $test_id = $db->create_new_test( 'zone5.rpcapi.example', {}, 10 );
ok( ! grep(/$test_id/, @batch_test_ids), 'new single test should not reuse batch tests' );
};
};
};
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;
}
}

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,237 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 4;
use Test::NoWarnings;
use Cwd;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
use JSON::Validator::Joi "joi";
use JSON::PP;
my $tempdir = tempdir( CLEANUP => 1 );
my $cwd = cwd();
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[PUBLIC PROFILES]
test = $cwd/t/test_profile.json
EOF
my $rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
sub test_validation {
my ( $method_name, $method_schema, $test_cases ) = @_;
subtest "Method $method_name" => sub {
for my $test_case (@$test_cases) {
subtest 'Test case: ' . $test_case->{name} => sub {
my @res = $rpcapi->validate_params( $method_schema, $test_case->{input});
is_deeply(\@res, $test_case->{output}, 'Matched validation output' ) or diag( encode_json \@res);
};
}
};
}
subtest 'Test JSON schema' => sub {
my $test_joi_schema = joi->new->object->strict->props(
hostname => joi->new->string->max(10)->required
);
my $test_raw_schema = {
type => 'object',
additionalProperties => 0,
required => [ 'hostname' ],
properties => {
hostname => {
type => 'string',
maxLength => 10
}
}
};
my $test_cases = [
{
name => 'Empty request',
input => {},
output => [{
message => 'Missing property',
path => '/hostname'
}]
},
{
name => 'Correct request',
input => {
hostname => 'example'
},
output => []
},
{
name => 'Bad request',
input => {
hostname => 'example.toolong'
},
output => [{
message => 'String is too long: 15/10.',
path => '/hostname'
}]
}
];
test_validation 'test_joi', $test_joi_schema, $test_cases;
test_validation 'test_raw', $test_raw_schema, $test_cases;
};
subtest 'Test custom error message' => sub {
my $test_custom_error_schema = {
type => 'object',
additionalProperties => 0,
required => [ 'hostname' ],
additionalProperties => 0,
properties => {
hostname => {
type => 'string',
'x-error-message' => 'Bad hostname, should be a string less than 10 characters long',
maxLength => 10
},
nameservers => {
type => 'array',
items => {
type => 'object',
required => [ 'ip' ],
additionalProperties => 0,
properties => {
ip => {
type => 'string',
'x-error-message' => 'Bad IP address',
pattern => '^[a-f0-9\.:]+$'
}
}
}
}
}
};
my $test_cases = [
{
name => 'Bad input',
input => {
hostname => 'This is a bad input',
nameservers => [
{ ip => 'Very bad indeed'},
{ ip => '10.10.10.10' },
{ ip => 'But not the previous property' }
]
},
output => [
{
path => '/hostname',
message => 'Bad hostname, should be a string less than 10 characters long',
},
{
path => '/nameservers/0/ip',
message => 'Bad IP address',
},
{
path => '/nameservers/2/ip',
message => 'Bad IP address',
}
]
}
];
test_validation 'test_custom_error', $test_custom_error_schema, $test_cases;
};
subtest 'Test custom formats' => sub {
my $test_extra_validator_schema = {
type => 'object',
properties => {
my_ip => {
type => 'string',
format => 'ip',
},
my_lang => {
type => 'string',
format => 'language_tag',
},
my_domain => {
type => 'string',
format => 'domain',
},
my_profile => {
type => 'string',
format => 'profile',
},
}
};
my $test_cases = [
{
name => 'Input ok',
input => {
my_ip => '192.0.2.1',
my_lang => 'en',
my_domain => 'zonemaster.net',
my_profile => 'test',
},
output => []
},
{
name => 'Bad ip',
input => {
my_ip => 'abc',
},
output => [{
path => '/my_ip',
message => 'Invalid IP address'
}]
},
{
name => 'Bad language format',
input => {
my_lang => 'abc',
},
output => [{
path => '/my_lang',
message => 'Invalid language tag format'
}]
},
{
name => 'Bad domain',
input => {
my_domain => 'not a domain',
},
output => [{
path => '/my_domain',
message => 'Domain name has an ASCII label ("not a domain") with a character not permitted.'
}]
},
{
name => 'Bad profile',
input => {
my_profile => 'other_profile',
},
output => [{
path => '/my_profile',
message => 'Unknown profile'
}]
},
];
test_validation 'test_extra_validator', $test_extra_validator_schema, $test_cases;
};

View File

@@ -0,0 +1,66 @@
#!perl
# This file is not included in the distribution package and not run
# at installation with cpanm().
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 '';
( $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();

View File

@@ -0,0 +1,86 @@
use strict;
use warnings;
use 5.14.2;
use Test::More tests => 2;
use Test::NoWarnings;
use Log::Any::Test;
use File::Basename qw( dirname );
use File::Spec::Functions qw( rel2abs );
use File::Temp qw( tempdir );
use Log::Any qw( $log );
use Test::Differences;
use Test::Exception;
use Zonemaster::Backend::Config;
use Zonemaster::Backend::DB qw( $TEST_RUNNING );
use Zonemaster::Engine;
my $t_path;
BEGIN {
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil;
my $db_backend = TestUtil::db_backend();
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[ZONEMASTER]
age_reuse_previous_test = 10
EOF
subtest 'Everything but Test::NoWarnings' => sub {
lives_ok { # Make sure we get to print log messages in case of errors.
my $db = TestUtil::init_db( $config );
subtest 'Claiming waiting tests for processing' => sub {
eq_or_diff
[ $db->get_test_request( undef ) ],
[ undef, undef ],
"An empty list is returned when queue is empty";
my $testid1 = $db->create_new_test( "1.claim.test", {}, 10 );
eq_or_diff
[ $db->get_test_request( undef ) ],
[ $testid1, undef ],
"A waiting test is returned if one is available";
eq_or_diff
[ $db->get_test_request( undef ) ],
[ undef, undef ],
"Claimed test is removed from queue";
is
$db->test_state( $testid1 ),
$TEST_RUNNING,
"Claimed test is in 'running' state";
};
};
};
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;
}
}

View File

@@ -0,0 +1,242 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 30;
use Test::NoWarnings;
use Cwd;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
use JSON::Validator::Joi "joi";
use JSON::PP;
###
### Setup
###
my $tempdir = tempdir( CLEANUP => 1 );
my $cwd = cwd();
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[PUBLIC PROFILES]
test = $cwd/t/test_profile.json
EOF
my $rpcapi = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
###
### JSONRPC request object construction helper
###
sub jsonrpc
{
my ($method, $params, $force_undef) = @_;
my $object = {
jsonrpc => '2.0',
id => 'testing',
method => $method
};
if (defined $params or $force_undef) {
$object->{params} = $params;
}
return $object;
}
###
### JSONRPC error response construction helpers
###
sub jsonrpc_error
{
my ($message, $code, $data, $id) = @_;
my $object = {
jsonrpc => '2.0',
id => $id,
error => {
message => $message,
code => $code
}
};
$object->{error}{data} = $data if defined $data;
return $object;
}
sub error_bad_jsonrpc
{
my ($data) = @_;
jsonrpc_error('The JSON sent is not a valid request object.', '-32600', $data, undef);
}
sub error_missing_params
{
jsonrpc_error("Missing 'params' object", '-32602', undef, 'testing');
}
sub error_bad_params
{
my ($messages) = @_;
my @data;
while (@$messages) {
my $path = shift @$messages;
my $message = shift @$messages;
push @data, { path => $path, message => $message };
}
jsonrpc_error('Invalid method parameter(s).', '-32602', \@data, 'testing');
}
sub no_error
{
return '';
}
###
### Test wrapper functions
###
sub test_validation
{
my ($input, $output, $message) = @_;
my $res = $rpcapi->jsonrpc_validate($input);
is_deeply($res, $output, $message) or diag(encode_json($res));
}
###
### The tests themselves
###
test_validation undef,
error_bad_jsonrpc('/: Expected object - got null.'),
"Sending undef is an error";
test_validation JSON::PP::false,
error_bad_jsonrpc('/: Expected object - got boolean.'),
"Sending a boolean is an error";
test_validation -1,
error_bad_jsonrpc('/: Expected object - got number.'),
"Sending a number is an error";
test_validation "hello",
error_bad_jsonrpc('/: Expected object - got string.'),
"Sending a string is an error";
test_validation [qw(a b c)],
error_bad_jsonrpc('/: Expected object - got array.'),
"Sending an array is an error";
test_validation {},
error_bad_jsonrpc('/jsonrpc: Missing property. /method: Missing property.'),
"Sending an empty object is an error";
test_validation { jsonrpc => '2.0' },
error_bad_jsonrpc('/method: Missing property.'),
"Sending an incomplete object is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions' },
error_bad_jsonrpc(''),
"Sending an object with no ID is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => JSON::PP::false },
error_bad_jsonrpc('/id: Expected null/number/string - got boolean.'),
"Sending an object whose ID is a boolean is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => [qw(a b c)] },
error_bad_jsonrpc('/id: Expected null/number/string - got array.'),
"Sending an object whose ID is an array is an error";
test_validation { jsonrpc => '2.0', method => 'system_versions', id => { a => 1 } },
error_bad_jsonrpc('/id: Expected null/number/string - got object.'),
"Sending an object whose ID is an object is an error";
test_validation jsonrpc("job_status"),
error_missing_params(),
"Calling job_status without parameters is an error";
test_validation jsonrpc("job_status", undef, 1),
error_bad_params(["/" => "Expected object - got null."]),
"Passing null as parameter to job_status is an error";
test_validation jsonrpc("job_status", JSON::PP::false),
error_bad_params(["/" => "Expected object - got boolean."]),
"Passing boolean as parameter to job_status is an error";
test_validation jsonrpc("job_status", 1),
error_bad_params(["/" => "Expected object - got number."]),
"Passing number as parameter to job_status is an error";
test_validation jsonrpc("job_status", "hello"),
error_bad_params(["/" => "Expected object - got string."]),
"Passing string as parameter to job_status is an error";
test_validation jsonrpc("job_status", [qw(a b c)]),
error_bad_params(["/" => "Expected object - got array."]),
"Passing array as parameter to job_status is an error";
test_validation jsonrpc("job_status", {}),
error_bad_params(["/job_id" => "Missing property"]),
"Passing empty object as parameter to job_status is an error";
test_validation jsonrpc("job_status", { job_id => 'this_will_definitely_never_ever_exist' }),
error_bad_params(["/job_id" => 'String does not match (?^u:^[0-9a-f]{16}$).']),
"Calling job_status with a bad job_id is an error";
test_validation jsonrpc("job_status", { job_id => '0123456789abcdef', data => "something" }),
error_bad_params(["/" => "Properties not allowed: data."]),
"Calling job_status with unknown parameters is an error";
test_validation jsonrpc("job_status", { job_id => '0123456789abcdef' }),
no_error,
"Calling job_status with a good job_id succeeds";
test_validation jsonrpc("system_versions"),
no_error,
"Calling system_versions with no parameters is OK";
test_validation jsonrpc("system_versions", undef, 1),
error_bad_params(["/" => "Expected object - got null."]),
"Passing null as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", JSON::PP::false),
error_bad_params(["/" => "Expected object - got boolean."]),
"Passing number as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", -1),
error_bad_params(["/" => "Expected object - got number."]),
"Passing number as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", "hello"),
error_bad_params(["/" => "Expected object - got string."]),
"Passing string as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", [qw(a b c)]),
error_bad_params(["/" => "Expected object - got array."]),
"Passing array as parameter to system_versions is an error";
test_validation jsonrpc("system_versions", { data => "something" }),
error_bad_params(["/" => "Properties not allowed: data."]),
"Calling system_versions with unrecognized parameter is an error";
test_validation jsonrpc("system_versions", {}),
no_error,
"Calling system_versions with empty object succeeds";

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,425 @@
use strict;
use warnings;
use 5.14.2;
my $t_path;
BEGIN {
use File::Spec::Functions qw( rel2abs );
use File::Basename qw( dirname );
$t_path = dirname( rel2abs( $0 ) );
}
use lib $t_path;
use TestUtil qw( RPCAPI TestAgent );
use Data::Dumper;
use File::Temp qw[tempdir];
use Test::Exception;
use Test::More; # see done_testing()
use Zonemaster::Engine;
use Zonemaster::Backend::Config;
my $db_backend = TestUtil::db_backend();
my $datafile = "$t_path/test01.data";
TestUtil::restore_datafile( $datafile );
my $tempdir = tempdir( CLEANUP => 1 );
my $configuration = <<"EOF";
[DB]
engine = $db_backend
[MYSQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[POSTGRESQL]
host = localhost
user = zonemaster_test
password = zonemaster
database = zonemaster_test
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US
EOF
if ( $ENV{ZONEMASTER_RECORD} ) {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_network_true.json
default=$t_path/test_profile_network_true.json
EOF
} else {
$configuration .= <<"EOF";
[PUBLIC PROFILES]
test_profile=$t_path/test_profile_no_network.json
default=$t_path/test_profile_no_network.json
EOF
}
my $config = Zonemaster::Backend::Config->parse( $configuration );
my $rpcapi = TestUtil::create_rpcapi( $config );
my $dbh = $rpcapi->{db}->dbh;
# Create the agent
my $agent = TestUtil::create_testagent( $config );
# define the default properties for the tests
my $params = {
client_id => 'Unit Test',
client_version => '1.0',
domain => 'afnic.fr',
ipv4 => JSON::PP::true,
ipv6 => JSON::PP::true,
profile => 'test_profile',
nameservers => [
{ ns => 'ns1.nic.fr' },
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' }
],
ds_info => [
{
keytag => 11627,
algorithm => 8,
digtype => 2,
digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448'
}
]
};
my $hash_id;
# This is the first test added to the DB, its 'id' is 1
my $test_id = 1;
subtest 'add a first test' => sub {
$hash_id = $rpcapi->start_domain_test( $params );
ok( $hash_id, "API start_domain_test OK" );
is( length($hash_id), 16, "Test has a 16 characters length hash ID (hash_id=$hash_id)" );
my ( $test_id_db, $hash_id_db ) = $dbh->selectrow_array( "SELECT id, hash_id FROM test_results WHERE id=?", undef, $test_id );
is( $test_id_db, $test_id , 'API start_domain_test -> Test inserted in the DB' );
is( $hash_id_db, $hash_id , 'Correct hash_id in database' );
# test test_progress API
my $progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 0 , 'Test has been created, its progress is 0' );
};
subtest 'get and run test' => sub {
my ( $hash_id_from_db ) = $rpcapi->{db}->get_test_request();
is( $hash_id_from_db, $hash_id, 'Get correct test to run' );
my $progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 1, 'Test has been picked, its progress is 1' );
diag "running the agent on test $hash_id";
$agent->run( $hash_id ); # blocking call
$progress = $rpcapi->test_progress( { test_id => $hash_id } );
is( $progress, 100 , 'Test has finished, its progress is 100' );
};
subtest 'API calls' => sub {
subtest 'get_test_results' => sub {
local $@ = undef;
my $res = eval { $rpcapi->get_test_results( { id => $hash_id, language => 'en' } ) };
if ( $@ ) {
fail 'Crashed while fetching job results: ' . Dumper( $@ );
}
ok( ! defined $res->{id}, 'Do not expose primary key' );
is( $res->{hash_id}, $hash_id, 'Retrieve the correct "hash_id"' );
ok( defined $res->{params}, 'Value "params" properly defined' );
ok( ! exists $res->{creation_time}, 'Key "creation_time" should be missing' );
ok( defined $res->{created_at}, 'Value "created_at" properly defined' );
ok( defined $res->{results}, 'Value "results" properly defined' );
if ( @{ $res->{results} } > 1 ) {
pass 'The test has some results';
}
else {
fail 'The test has some results: ' . Dumper( $res->{results} );
}
};
subtest 'get_test_params' => sub {
my $res = $rpcapi->get_test_params( { test_id => $hash_id } );
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
is( $res->{profile}, $params->{profile}, 'Retrieve the correct "profile" value' );
is( $res->{client_id}, $params->{client_id}, 'Retrieve the correct "client_id" value' );
is( $res->{client_version}, $params->{client_version}, 'Retrieve the correct "client_version" value' );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
is_deeply( $res->{ds_info}, $params->{ds_info}, 'Retrieve the correct "ds_info" value' );
};
subtest 'add_api_user' => sub {
my $res;
eval {
$res = $rpcapi->add_api_user( { username => "zonemaster_test", api_key => "zonemaster_test's api key" } );
};
is( $res, 1, 'API add_api_user success');
my $user_check_query = q/SELECT * FROM users WHERE username = 'zonemaster_test'/;
is( scalar( $dbh->selectrow_array( $user_check_query ) ), 1 ,'API add_api_user user created' );
};
subtest 'version_info' => sub {
my $res = $rpcapi->version_info();
ok( defined( $res->{zonemaster_ldns} ), 'Has a "zonemaster_ldns" key' );
ok( defined( $res->{zonemaster_engine} ), 'Has a "zonemaster_engine" key' );
ok( defined( $res->{zonemaster_backend} ), 'Has a "zonemaster_backend" key' );
};
subtest 'profile_names' => sub {
my $res = $rpcapi->profile_names();
is( scalar( @$res ), 2, 'There are exactly 2 public profiles' );
ok( grep( /default/, @$res ), 'The profile "default" is defined' );
ok( grep( /test_profile/, @$res ), 'The profile "test_profile" is defined' );
};
subtest 'get_data_from_parent_zone' => sub {
my $res = $rpcapi->get_data_from_parent_zone( { domain => "fr" } );
note explain( $res );
ok( defined( $res->{ns_list} ), 'Has a list of nameservers' );
ok( defined( $res->{ds_list} ), 'Has a list of DS records' );
my @ns_list = map { $_->{ns} } @{ $res->{ns_list} };
ok( grep( /d\.nic\.fr/, @ns_list ), 'Has "d.nic.fr" nameserver' );
ok( grep( /f\.ext\.nic\.fr/, @ns_list ), 'Has "f.ext.nic.fr" nameserver' );
ok( grep( /g\.ext\.nic\.fr/, @ns_list ), 'Has "g.ext.nic.fr" nameserver' );
my @ip_list = map { $_->{ip} } @{ $res->{ns_list} };
ok( grep( /194\.0\.9\.1/, @ip_list ), 'Has "194.0.9.1" ip' ); # d.nic.fr
ok( grep( /2001:678:c::1/, @ip_list ), 'Has "2001:678:c::1" ip' );
ok( grep( /194\.0\.36\.1/, @ip_list ), 'Has "194.0.36.1" ip' ); # g.ext.nic.fr
ok( grep( /2001:678:4c::1/, @ip_list ), 'Has "2001:678:4c::1" ip' );
ok( grep( /194\.146\.106\.46/, @ip_list ), 'Has "194.146.106.46" ip' ); # f.ext.nic.fr
ok( grep( /2001:67c:1010:11::53/, @ip_list ), 'Has "2001:67c:1010:11::53" ip' );
my $ds_value = {
'algorithm' => 13,
'digest' => '1303e8da8fb60db500d5bea1ee5dc9a2bcc93dfe2fc43d346576658feccf5749', # must match case
'digtype' => 2,
'keytag' => 29133
};
is( scalar( @{ $res->{ds_list} } ), 1, 'Has only one DS set' );
is_deeply( $res->{ds_list}[0], $ds_value, 'Has correct DS values' );
};
};
# start a second test with IPv6 disabled
$params->{ipv6} = 0;
$hash_id = $rpcapi->start_domain_test( $params );
$rpcapi->{db}->claim_test( $hash_id )
or BAIL_OUT( "test needs to be claimed before calling run()" );
diag "running the agent on test $hash_id";
$agent->run($hash_id);
subtest 'second test has IPv6 disabled' => sub {
my $res = $rpcapi->get_test_params( { test_id => $hash_id } );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
$res = $rpcapi->get_test_results( { id => $hash_id, language => 'en' } );
my @msgs = map { $_->{message} } @{ $res->{results} };
ok( grep( /IPv6 is disabled/, @msgs ), 'Results contain an "IPv6 is disabled" message' );
};
my $test_history;
subtest 'get_test_history' => sub {
my $offset = 0;
my $limit = 10;
my $method_params = {
frontend_params => { domain => $params->{domain} },
offset => $offset,
limit => $limit
};
$test_history = $rpcapi->get_test_history( $method_params );
note explain( $test_history );
is( scalar( @$test_history ), 2, 'Two tests created' );
foreach my $res (@$test_history) {
is( length($res->{id}), 16, 'Test has 16 characters length hash ID' );
is( $res->{undelegated}, JSON::PP::true, 'Test is undelegated' );
ok( ! exists $res->{creation_time}, 'Key "creation_time" should be missing' );
ok( defined $res->{created_at}, 'Value "created_at" properly defined' );
ok( defined $res->{overall_result}, 'Value "overall_result" properly defined' );
}
subtest 'include finished tests only' => sub {
# start a thirs test with IPv4 disabled
$params->{ipv6} = 1;
$params->{ipv4} = 0;
# create the test, retrieve its id but we don't run it
$rpcapi->start_domain_test( $params );
( $hash_id ) = $rpcapi->{db}->get_test_request();
$test_history = $rpcapi->get_test_history( $method_params );
note explain( $test_history );
is( scalar( @$test_history ), 2, 'Only 2 tests should be retrieved' );
# now run the test
diag "running the agent on test $hash_id";
$agent->run( $hash_id );
$test_history = $rpcapi->get_test_history( $method_params );
is( scalar( @$test_history ), 3, 'Now 3 tests should be retrieved' );
}
};
subtest 'mock another client (i.e. reuse a previous test)' => sub {
$params->{client_id} = 'Another Client';
$params->{client_version} = '0.1';
my $new_hash_id = $rpcapi->start_domain_test( $params );
is( $new_hash_id, $hash_id, 'Has the same hash than previous test' );
subtest 'check test_params values' => sub {
my $res = $rpcapi->get_test_params( { test_id => "$hash_id" } );
# the following values are part of the fingerprint
is( $res->{domain}, $params->{domain}, 'Retrieve the correct "domain" value' );
is( $res->{profile}, $params->{profile}, 'Retrieve the correct "profile" value' );
is( $res->{ipv4}, $params->{ipv4}, 'Retrieve the correct "ipv4" value' );
is( $res->{ipv6}, $params->{ipv6}, 'Retrieve the correct "ipv6" value' );
is_deeply( $res->{nameservers}, $params->{nameservers}, 'Retrieve the correct "nameservers" value' );
is_deeply( $res->{ds_info}, $params->{ds_info}, 'Retrieve the correct "ds_info" value' );
# both client_id and client_version are different since an old test has been reused
isnt( $res->{client_id}, $params->{client_id}, 'The "client_id" value is not the same (which is fine)' );
isnt( $res->{client_version}, $params->{client_version}, 'The "client_version" value is not the same (which is fine)' );
};
};
subtest 'check historic tests' => sub {
# Verifies that delegated and undelegated tests are coded correctly when started
# and that the filter option in "get_test_history" works correctly
my $domain = 'xa';
# Non-batch for "start_domain_test":
my $params_un1 = { # undelegated, non-batch
domain => $domain,
nameservers => [
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
],
};
my $params_un2 = { # undelegated, non-batch
domain => $domain,
ds_info => [
{ keytag => 11627, algorithm => 8, digtype => 2, digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448' },
],
};
my $params_dn1 = { # delegated, non-batch
domain => $domain,
};
# Batch for "add_batch_job"
my $domain2 = 'xb';
my $params_ub1 = { # undelegated, batch
domains => [ $domain, $domain2 ],
test_params => {
nameservers => [
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
],
},
};
my $params_ub2 = { # undelegated, batch
domains => [ $domain, $domain2 ],
test_params => {
ds_info => [
{ keytag => 11627, algorithm => 8, digtype => 2, digest => 'a6cca9e6027ecc80ba0f6d747923127f1d69005fe4f0ec0461bd633482595448' },
],
},
};
my $params_db1 = { # delegated, batch
domains => [ $domain, $domain2 ],
};
# The batch jobs, $params_ub1, $params_ub2 and $params_db1, cannot be run from here due to limitation in the API. See issue #827.
foreach my $param ($params_un1, $params_un2, $params_dn1) {
my $testid = $rpcapi->start_domain_test( $param );
ok( $testid, "API start_domain_test ID OK" );
$rpcapi->{db}->claim_test( $testid )
or BAIL_OUT( "test needs to be claimed before calling run()" );
diag "running the agent on test $testid";
$agent->run( $testid );
is( $rpcapi->test_progress( { test_id => $testid } ), 100 , 'API test_progress -> Test finished' );
};
my $test_history_delegated = $rpcapi->get_test_history(
{
filter => 'delegated',
frontend_params => {
domain => $domain,
}
} );
my $test_history_undelegated = $rpcapi->get_test_history(
{
filter => 'undelegated',
frontend_params => {
domain => $domain,
}
} );
note explain( $test_history_delegated );
is( scalar( @$test_history_delegated ), 1, 'One delegated test created' );
note explain( $test_history_undelegated );
is( scalar( @$test_history_undelegated ), 2, 'Two undelegated tests created' );
subtest 'domain is case and trailing dot insensitive' => sub {
my $test_history_delegated = $rpcapi->get_test_history(
{
filter => 'delegated',
frontend_params => {
domain => $domain . '.',
}
} );
my $test_history_undelegated = $rpcapi->get_test_history(
{
filter => 'undelegated',
frontend_params => {
domain => ucfirst( $domain ),
}
} );
is( scalar( @$test_history_delegated ), 1, 'One delegated test created' );
is( scalar( @$test_history_undelegated ), 2, 'Two undelegated tests created' );
};
};
subtest 'normalize "domain" column' => sub {
my %domains_to_test = (
"aFnIc.Fr" => "afnic.fr",
"afnic.fr." => "afnic.fr",
"aFnic.Fr." => "afnic.fr"
);
my $test_params = {
client_id => 'Unit Test',
client_version => '1.0',
};
while ( my ($domain, $expected) = each (%domains_to_test) ) {
$test_params->{domain} = $domain;
$hash_id = $rpcapi->start_domain_test( $test_params );
my ( $db_domain ) = $dbh->selectrow_array( "SELECT domain FROM test_results WHERE hash_id=?", undef, $hash_id );
is( $db_domain, $expected, 'stored domain name is normalized' );
}
};
TestUtil::save_datafile( $datafile );
done_testing();

View File

@@ -0,0 +1,84 @@
{
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,85 @@
{
"no_network" : false,
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,85 @@
{
"no_network" : true,
"logfilter": {
"BASIC":{
"IPV6_DISABLED" : [
{
"when": {
"rrtype": [ "SOA", "NS" ]
},
"set": "INFO"
}
]
}
},
"test_cases": [
"address01",
"address02",
"address03",
"basic00",
"basic01",
"basic02",
"basic03",
"connectivity01",
"connectivity02",
"connectivity03",
"consistency01",
"consistency02",
"consistency03",
"consistency04",
"consistency05",
"consistency06",
"dnssec01",
"dnssec02",
"dnssec03",
"dnssec04",
"dnssec05",
"dnssec07",
"dnssec06",
"dnssec08",
"dnssec09",
"dnssec10",
"dnssec11",
"dnssec13",
"dnssec14",
"dnssec15",
"dnssec16",
"dnssec17",
"dnssec18",
"delegation01",
"delegation02",
"delegation03",
"delegation04",
"delegation05",
"delegation06",
"delegation07",
"nameserver01",
"nameserver02",
"nameserver04",
"nameserver05",
"nameserver06",
"nameserver07",
"nameserver10",
"nameserver11",
"nameserver12",
"nameserver13",
"syntax01",
"syntax02",
"syntax03",
"syntax04",
"syntax05",
"syntax06",
"syntax07",
"syntax08",
"zone01",
"zone02",
"zone03",
"zone04",
"zone05",
"zone06",
"zone07",
"zone08",
"zone09",
"zone10"
]
}

View File

@@ -0,0 +1,289 @@
use strict;
use warnings;
use 5.14.2;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Encode;
use File::ShareDir qw[dist_file];
use JSON::PP;
use File::Temp qw[tempdir];
use Zonemaster::Backend::Config;
use Zonemaster::Backend::RPCAPI;
my $tempdir = tempdir( CLEANUP => 1 );
my $config = Zonemaster::Backend::Config->parse( <<EOF );
[DB]
engine = SQLite
[SQLITE]
database_file = $tempdir/zonemaster.sqlite
[LANGUAGE]
locale = en_US fr_FR da_DK fi_FI nb_NO sl_SI sv_SE
EOF
my $engine = Zonemaster::Backend::RPCAPI->new(
{
dbtype => $config->DB_engine,
config => $config,
}
);
sub start_domain_validate_params {
return $engine->validate_params( $Zonemaster::Backend::RPCAPI::json_schemas{start_domain_test}, @_ );
}
subtest 'Everything but NoWarnings' => sub {
my $can_use_threads = eval 'use threads; 1';
my $frontend_params = {
ipv4 => 1,
ipv6 => 1,
};
$frontend_params->{nameservers} = [ # list of the namaserves up to 32
{ ns => 'ns1.nic.fr', ip => '1.2.3.4' }, # key values pairs representing nameserver => namesterver_ip
{ ns => 'ns2.nic.fr', ip => '192.134.4.1' },
];
subtest 'domain present' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'afnic.fr'
}
);
is( scalar @res, 0 );
};
subtest 'consecutive dots' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'afnic..fr'
}
);
is( scalar @res, 1 );
};
subtest encode_utf8( 'idn domain=[é]' ) => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'é'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest encode_utf8( 'idn domain=[éé]' ) => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => 'éé'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '253 characters long domain without dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '254 characters long domain with trailing dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com.'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '254 characters long domain without trailing dot' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.club'
}
);
cmp_ok( scalar @res, '>', 0 )
or diag( encode_json @res );
};
subtest '63 characters long domain label' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '012345678901234567890123456789012345678901234567890123456789-63.fr'
}
);
is( scalar @res, 0 )
or diag( encode_json @res );
};
subtest '64 characters long domain label' => sub {
my @res = start_domain_validate_params(
{
%$frontend_params, domain => '012345678901234567890123456789012345678901234567890123456789--64.fr'
}
);
cmp_ok( scalar @res, '>', 0 )
or diag( encode_json @res );
};
#TEST NS
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
# domain present?
$frontend_params->{nameservers}->[0]->{ns} = 'afnic.fr';
is( scalar start_domain_validate_params( $frontend_params ), 0, 'domain present' );
# idn
$frontend_params->{nameservers}->[0]->{ns} = 'é';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'idn domain=[é]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# idn
$frontend_params->{nameservers}->[0]->{ns} = 'éé';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'idn domain=[éé]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 253 characters long domain without dot
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '253 characters long domain without dot' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 254 characters long domain with trailing dot
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.com.';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '254 characters long domain with trailing dot' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 254 characters long domain without trailing
$frontend_params->{nameservers}->[0]->{ns} =
'123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.123456789.club';
cmp_ok(
scalar start_domain_validate_params( $frontend_params ), '>', 0,
encode_utf8( '254 characters long domain without trailing dot' )
) or diag( encode_jsonstart_domain_validate_params( $frontend_params ) );
# 63 characters long domain label
$frontend_params->{nameservers}->[0]->{ns} = '012345678901234567890123456789012345678901234567890123456789-63.fr';
is(
scalar start_domain_validate_params( $frontend_params ), 0,
encode_utf8( '63 characters long domain label' )
) or diag( encode_json start_domain_validate_params( $frontend_params ) );
# 64 characters long domain label
$frontend_params->{nameservers}->[0]->{ns} = '012345678901234567890123456789012345678901234567890123456789-64-.fr';
cmp_ok( scalar start_domain_validate_params( $frontend_params ), '>', 0,
encode_utf8( '64 characters long domain label' ) )
or diag(encode_json start_domain_validate_params( $frontend_params ) );
# DELEGATED TEST
delete( $frontend_params->{nameservers} );
$frontend_params->{domain} = 'afnic.fr';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'delegated domain exists' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# IP ADDRESS FORMAT
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ns} = 'ns1.nic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid IPV4' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4444';
cmp_ok( scalar start_domain_validate_params( $frontend_params ), '>', 0, encode_utf8( 'Invalid IPV4' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = 'fe80::6ef0:49ff:fe7b:e4bb';
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid IPV6' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{nameservers}->[0]->{ip} = 'fe80::6ef0:49ff:fe7b:e4bbffffff';
cmp_ok( start_domain_validate_params( $frontend_params ), '>', 0, encode_utf8( 'Invalid IPV6' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
# DS
$frontend_params->{domain} = 'afnic.fr';
$frontend_params->{nameservers}->[0]->{ns} = 'ns1.nic.fr';
$frontend_params->{nameservers}->[0]->{ip} = '1.2.3.4';
$frontend_params->{ds_info}->[0]->{algorithm} = 1;
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
$frontend_params->{ds_info}->[0]->{digtype} = 1;
$frontend_params->{ds_info}->[0]->{keytag} = 5000;
is( scalar start_domain_validate_params( $frontend_params ), 0, encode_utf8( 'Valid Algorithm Type [numeric format]' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{algorithm} = 'a';
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid Algorithm Type' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{algorithm} = 1;
$frontend_params->{ds_info}->[0]->{digest} = '01234567890123456789012345678901234567890';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest length' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digest} = 'Z123456789012345678901234567890123456789';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest format' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digest} = '0123456789012345678901234567890123456789';
$frontend_params->{ds_info}->[0]->{digtype} = -1;
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid digest type' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{digtype} = 1;
$frontend_params->{ds_info}->[0]->{keytag} = 'not a int';
is( scalar start_domain_validate_params( $frontend_params ), 1, encode_utf8( 'Invalid keytag' ) )
or diag( encode_json start_domain_validate_params( $frontend_params ) );
$frontend_params->{ds_info}->[0]->{keytag} = 5000;
{
local $frontend_params->{language} = "zz";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language, "zz" unknown' ) or diag( explain \@res );
}
{
local $frontend_params->{language} = "fr-FR";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language tag syntax' ) or diag( explain \@res );
}
{
local $frontend_params->{language} = "nb_NO";
my @res = start_domain_validate_params( $frontend_params );
is( scalar @res, 1, 'Invalid language tag syntax' ) or diag( explain \@res );
}
};

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env perl
use v5.16;
use warnings;
use utf8;
use Test::More;
use Locale::Messages qw( LC_ALL );
use POSIX qw( setlocale );
BEGIN {
# Set correct locale for translation in case not set in calling environment
delete $ENV{"LANG"};
delete $ENV{"LANGUAGE"};
delete $ENV{"LC_CTYPE"};
delete $ENV{"LC_MESSAGES"};
setlocale( LC_ALL, "C.UTF-8" );
use_ok( 'Zonemaster::Backend::Translator' )
or BAIL_OUT "Cannot continue without translator module";
}
my $translator = Zonemaster::Backend::Translator->instance();
isa_ok $translator, 'Zonemaster::Backend::Translator', "Zonemaster::Backend::Translator->instance()"
or BAIL_OUT "Cannot continue without a translator instance";
subtest 'Basic tests' => sub {
isa_ok 'Zonemaster::Backend::Translator', 'Zonemaster::Engine::Translator';
my $locale = 'fr_FR.UTF-8';
ok( $translator->locale( $locale ), "Setting locale to '$locale' works" );
};
subtest 'Testing some translations' => sub {
my $message = {
module => 'System',
testcase => 'Unspecified',
timestamp => '0.000778913497924805',
level => 'INFO',
tag => 'GLOBAL_VERSION',
args => { version => 'v5.0.0' }
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\AUtilisation de la version .* du moteur Zonemaster\.\Z/, 'Translating a GLOBAL_VERSION message tag works';
};
subtest 'Test a message translation from Engine with non-ASCII strings' => sub {
my $message = {
module => 'Basic',
testcase => 'Basic02',
timestamp => '4.085114956678410350',
level => 'ERROR',
tag => 'B02_NS_BROKEN',
args => { ns => 'ns1.example' }
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\ARéponse cassée du serveur de noms /, 'Translating a B02_NS_BROKEN message works';
like $translation, qr/cass\x{e9}e/, 'Translation is a string of Unicode codepoints, not bytes';
};
subtest 'Test a Backend-specific translation' => sub {
my $message = {
module => 'Backend',
testcase => '',
timestamp => '59',
level => 'CRITICAL',
tag => 'TEST_DIED',
args => {}
};
my $translation = $translator->translate_tag( $message );
like $translation, qr/\AUne erreur est survenue /, 'Translating a backend-specific TEST_DIED message tag works';
};
subtest 'Test a test case translation with non-ASCII strings' => sub {
my $translation = $translator->test_case_description( 'Consistency01' );
like $translation, qr/\ACoh\x{e9}rence du num\x{e9}ro de s\x{e9}rie/, 'Translating Consistency01 gives a string of Unicode codepoints';
};
done_testing;

View File

@@ -0,0 +1,215 @@
#!perl -T
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use Test::Differences;
use Scalar::Util qw( tainted );
use JSON::Validator::Schema::Draft7;
# Get a tainted copy of a string
sub taint {
my ( $string ) = @_;
if ( !tainted $0 ) {
BAIL_OUT( 'We need $0 to be tainted' );
}
return substr $string . $0, length $0;
}
sub compile_schema {
my $jv = JSON::Validator::Schema::Draft7->new->coerce('booleans,numbers,strings')->data(@_);
$jv->formats(Zonemaster::Backend::Validator::formats( undef ));
return $jv;
}
subtest 'Everything but NoWarnings' => sub {
use_ok( 'Zonemaster::Backend::Validator', ':untaint' );
subtest 'ds_info' => sub {
my $v = compile_schema( Zonemaster::Backend::Validator->new->ds_info );
my $ds_info_40 = { digest => '0' x 40, algorithm => 0, digtype => 0, keytag => 0 };
my $ds_info_64 = { digest => '0' x 64, algorithm => 0, digtype => 0, keytag => 0 };
eq_or_diff [ $v->validate( $ds_info_40 ) ], [], 'accept ds_info with 40-digit hash';
eq_or_diff [ $v->validate( $ds_info_64 ) ], [], 'accept ds_info with 64-digit hash';
};
subtest 'ip_address' => sub {
my $v = compile_schema( Zonemaster::Backend::Validator->new->ip_address );
eq_or_diff [ $v->validate( '192.168.0.2' ) ], [], 'accept: 192.168.0.2';
eq_or_diff [ $v->validate( '2001:db8::1' ) ], [], 'accept: 2001:db8::1';
};
subtest 'untaint_abs_path' => sub {
is scalar untaint_abs_path( '/var/db/zonemaster.sqlite' ), '/var/db/zonemaster.sqlite', 'accept: /var/db/zonemaster.sqlite';
is scalar untaint_abs_path( 'zonemaster.sqlite' ), undef, 'reject: zonemaster.sqlite';
is scalar untaint_abs_path( './zonemaster.sqlite' ), undef, 'reject: ./zonemaster.sqlite';
ok !tainted( untaint_abs_path( taint( 'localhost' ) ) ), 'launder taint';
};
subtest 'untaint_engine_type' => sub {
is scalar untaint_engine_type( 'MySQL' ), 'MySQL', 'accept: MySQL';
is scalar untaint_engine_type( 'mysql' ), 'mysql', 'accept: mysql';
is scalar untaint_engine_type( 'PostgreSQL' ), 'PostgreSQL', 'accept: PostgreSQL';
is scalar untaint_engine_type( 'postgresql' ), 'postgresql', 'accept: postgresql';
is scalar untaint_engine_type( 'SQLite' ), 'SQLite', 'accept: SQLite';
is scalar untaint_engine_type( 'sqlite' ), 'sqlite', 'accept: sqlite';
is scalar untaint_engine_type( 'Excel' ), undef, 'reject: Excel';
ok !tainted( untaint_engine_type( taint( 'SQLite' ) ) ), 'launder taint';
};
subtest 'untaint_ip_address' => sub {
is scalar untaint_ip_address( '192.0.2.1' ), '192.0.2.1', 'accept: 192.0.2.1';
is scalar untaint_ip_address( '192.0.2' ), undef, 'reject: 192.0.2';
is scalar untaint_ip_address( '192' ), undef, 'reject: 192';
is scalar untaint_ip_address( '192.0.2.1:3306' ), undef, 'reject: 192.0.2.1:3306';
is scalar untaint_ip_address( '2001:db8::' ), '2001:db8::', 'accept: 2001:db8::';
is scalar untaint_ip_address( '2001:db8::/32' ), undef, 'reject: 2001:db8::/32';
is scalar untaint_ip_address( '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff' ), '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff', 'accept: 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff';
is scalar untaint_ip_address( '2001:db8:ffff:ffff:ffff:ffff:ffff' ), undef, 'reject: 2001:db8:ffff:ffff:ffff:ffff:ffff';
is scalar untaint_ip_address( '2001:db8::255.255.255.254' ), '2001:db8::255.255.255.254', 'accept: 2001:db8::255.255.255.254';
is scalar untaint_ip_address( '2001:db8::255.255.255' ), undef, 'reject: 2001:db8::255.255.255';
is scalar untaint_ip_address( '::1' ), '::1', 'accept: ::1';
is scalar untaint_ip_address( ':::1' ), undef, 'reject: :::1';
ok !tainted( untaint_ip_address( taint( '192.0.2.1' ) ) ), 'launder taint';
};
subtest 'untaint_ldh_domain' => sub {
is scalar untaint_ldh_domain( 'localhost' ), 'localhost', 'accept: localhost';
is scalar untaint_ldh_domain( 'example.com' ), 'example.com', 'accept: example.com';
is scalar untaint_ldh_domain( 'example.com.' ), 'example.com.', 'accept: example.com.';
is scalar untaint_ldh_domain( '192.0.2.1' ), '192.0.2.1', 'accept: 192.0.2.1';
is scalar untaint_ldh_domain( '0/26.2.0.192.in-addr.arpa' ), '0/26.2.0.192.in-addr.arpa', 'accept: 0/26.2.0.192.in-addr.arpa';
is scalar untaint_ldh_domain( '_http._tcp.example.com' ), '_http._tcp.example.com', 'accept: _http._tcp.example.com';
is scalar untaint_ldh_domain( '192.0.2.1:3306' ), undef, 'reject: 192.0.2.1:3306';
is scalar untaint_ldh_domain( '1!26.2.0.192.in-addr.arpa' ), undef, 'reject: 1!26.2.0.192.in-addr.arpa';
is scalar untaint_ldh_domain( '$http.example.com' ), undef, 'reject: $http.example.com';
ok !tainted( untaint_ldh_domain( taint( 'localhost' ) ) ), 'launder taint';
};
subtest 'untaint_locale_tag' => sub {
is scalar untaint_locale_tag( 'en_US' ), 'en_US', 'accept: en_US';
is scalar untaint_locale_tag( 'en' ), undef, 'reject: en';
is scalar untaint_locale_tag( 'English' ), undef, 'reject: English';
ok !tainted( untaint_locale_tag( taint( 'en_US' ) ) ), 'launder taint';
};
subtest 'untaint_mariadb_database' => sub {
is scalar untaint_mariadb_database( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_mariadb_database( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_mariadb_database( 'dollar$' ), 'dollar$', 'accept: dollar$';
is scalar untaint_mariadb_database( '$dollar' ), '$dollar', 'accept: $dollar';
is scalar untaint_mariadb_database( '0zonemaster' ), '0zonemaster', 'accept: 0zonemaster';
is scalar untaint_mariadb_database( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_mariadb_database( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_mariadb_database( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_mariadb_database( '' ), undef, 'reject empty string';
is scalar untaint_mariadb_database( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_mariadb_database( 'a' x 65 ), undef, 'reject 65 characters';
is scalar untaint_mariadb_database( 'a' x 64 ), 'a' x 64, 'accept 64 characters';
ok !tainted( untaint_mariadb_database( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_mariadb_user' => sub {
is scalar untaint_mariadb_user( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_mariadb_user( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_mariadb_user( '$dollar' ), '$dollar', 'accept: $dollar';
is scalar untaint_mariadb_user( '0zonemaster' ), '0zonemaster', 'accept: 0zonemaster';
is scalar untaint_mariadb_user( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_mariadb_user( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_mariadb_user( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_mariadb_user( '' ), undef, 'reject empty string';
is scalar untaint_mariadb_user( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_mariadb_user( 'a' x 81 ), undef, 'reject 81 characters';
is scalar untaint_mariadb_user( 'a' x 80 ), 'a' x 80, 'accept 80 characters';
ok !tainted( untaint_mariadb_user( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_password' => sub {
is scalar untaint_password( '123456' ), '123456', 'accept: 123456';
is scalar untaint_password( 'password' ), 'password', 'accept: password';
is scalar untaint_password( '!@#$%^&*<' ), '!@#$%^&*<', 'accept: !@#$%^&*<';
is scalar untaint_password( 'Qwertyuiop' ), 'Qwertyuiop', 'accept: Qwertyuiop';
is scalar untaint_password( 'battery staple' ), 'battery staple', 'accept: battery staple';
is scalar untaint_password( '' ), '', 'accept the empty string';
is scalar untaint_password( "\t" ), undef, 'reject tab character';
is scalar untaint_password( "\x80" ), undef, 'reject del character';
is scalar untaint_password( ' x' ), undef, 'reject initial space';
is scalar untaint_password( '<x' ), undef, 'reject initial <';
is scalar untaint_password( 'åäö' ), undef, 'reject: åäö';
is scalar untaint_password( 'a' x 100 ), 'a' x 100, 'accept 100 characters';
is scalar untaint_password( 'a' x 101 ), undef, 'reject 101 characters';
ok !tainted( untaint_password( taint( '123456' ) ) ), 'launder taint';
};
subtest 'untaint_postgresql_ident' => sub {
is scalar untaint_postgresql_ident( 'zonemaster' ), 'zonemaster', 'accept: zonemaster';
is scalar untaint_postgresql_ident( 'ZONEMASTER' ), 'ZONEMASTER', 'accept: ZONEMASTER';
is scalar untaint_postgresql_ident( 'zm_backend' ), 'zm_backend', 'accept: zm_backend';
is scalar untaint_postgresql_ident( 'dollar$' ), 'dollar$', 'accept: dollar$';
is scalar untaint_postgresql_ident( '$dollar' ), undef, 'reject: $dollar';
is scalar untaint_postgresql_ident( 'zm backend' ), undef, 'reject: zm backend';
is scalar untaint_postgresql_ident( '0zonemaster' ), undef, 'reject: 0zonemaster';
is scalar untaint_postgresql_ident( 'zm-backend' ), undef, 'reject: zm-backend';
is scalar untaint_postgresql_ident( '' ), undef, 'reject empty string';
is scalar untaint_postgresql_ident( 'zönemästër' ), undef, 'reject: zönemästër';
is scalar untaint_postgresql_ident( 'a' x 64 ), undef, 'reject 64 characters';
is scalar untaint_postgresql_ident( 'a' x 63 ), 'a' x 63, 'accept 63 characters';
ok !tainted( untaint_postgresql_ident( taint( 'zonemaster' ) ) ), 'launder taint';
};
subtest 'untaint_profile_name' => sub {
is scalar untaint_profile_name( 'default' ), 'default', 'accept: default';
is scalar untaint_profile_name( '-leading-dash' ), undef, 'reject: -leading-dash';
is scalar untaint_profile_name( 'trailing-dash-' ), undef, 'reject: trailing-dash-';
is scalar untaint_profile_name( 'middle-dash' ), 'middle-dash', 'accept: middle-dash';
is scalar untaint_profile_name( '_leading_underscore' ), undef, 'reject: _leading_underscore';
is scalar untaint_profile_name( 'trailing_underscore_' ), undef, 'reject: trailing_underscore_';
is scalar untaint_profile_name( 'middle_underscore' ), 'middle_underscore', 'accept: middle_underscore';
is scalar untaint_profile_name( '0-leading-digit' ), '0-leading-digit', 'accept: 0-leading-digit';
is scalar untaint_profile_name( 'a' ), 'a', 'accept: a';
is scalar untaint_profile_name( '-' ), undef, 'reject dash';
is scalar untaint_profile_name( '_' ), undef, 'reject underscore';
is scalar untaint_profile_name( 'a' x 32 ), 'a' x 32, 'accept 32 characters';
is scalar untaint_profile_name( 'a' x 33 ), undef, 'reject 33 characters';
ok !tainted( untaint_profile_name( taint( 'default' ) ) ), 'launder taint';
};
subtest 'untaint_non_negative_int' => sub {
is scalar untaint_non_negative_int( '1' ), '1', 'accept: 1';
is scalar untaint_non_negative_int( '0' ), '0', 'accept: 0';
is scalar untaint_non_negative_int( '99999' ), '99999', 'accept: 99999';
is scalar untaint_non_negative_int( '100000' ), undef, 'reject: 100000';
is scalar untaint_non_negative_int( '0.5' ), undef, 'reject: 0.5';
is scalar untaint_non_negative_int( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_non_negative_int( taint( '1' ) ) ), 'launder taint';
};
subtest 'untaint_strictly_positive_int' => sub {
is scalar untaint_strictly_positive_int( '1' ), '1', 'accept: 1';
is scalar untaint_strictly_positive_int( '99999' ), '99999', 'accept: 99999';
is scalar untaint_strictly_positive_int( '100000' ), undef, 'reject: 100000';
is scalar untaint_strictly_positive_int( '0' ), undef, 'reject: 0';
is scalar untaint_strictly_positive_int( '0.5' ), undef, 'reject: 0.5';
is scalar untaint_strictly_positive_int( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_strictly_positive_int( taint( '1' ) ) ), 'launder taint';
};
subtest 'untaint_strictly_positive_millis' => sub {
is scalar untaint_strictly_positive_millis( '0.5' ), '0.5', 'accept: 0.5';
is scalar untaint_strictly_positive_millis( '0.001' ), '0.001', 'accept: 0.001';
is scalar untaint_strictly_positive_millis( '99999.999' ), '99999.999', 'accept: 99999.999';
is scalar untaint_strictly_positive_millis( '1' ), '1', 'accept: 1';
is scalar untaint_strictly_positive_millis( '99999' ), '99999', 'accept: 99999';
is scalar untaint_strictly_positive_millis( '0.0009' ), undef, 'reject: 0.0009';
is scalar untaint_strictly_positive_millis( '100000' ), undef, 'reject: 100000';
is scalar untaint_strictly_positive_millis( '0' ), undef, 'reject: 0';
is scalar untaint_strictly_positive_millis( '0.0' ), undef, 'reject: 0.0';
is scalar untaint_strictly_positive_millis( '-1' ), undef, 'reject: -1';
ok !tainted( untaint_strictly_positive_millis( taint( '0.5' ) ) ), 'launder taint';
};
};