Merge pull request #741 from rhansen/tests

Unit test improvements
This commit is contained in:
Richard Hansen 2024-09-06 18:51:44 -04:00 committed by GitHub
commit 490dc16d33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 492 additions and 457 deletions

View file

@ -162,4 +162,6 @@ EXTRA_DIST += $(handwritten_tests) \
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-cert.pem \
t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \ t/lib/ddclient/Test/Fake/HTTPD/dummy-server-key.pem \
t/lib/ddclient/t.pm \ t/lib/ddclient/t.pm \
t/lib/ddclient/t/HTTPD.pm \
t/lib/ddclient/t/ip.pm \
t/lib/ok.pm t/lib/ok.pm

View file

@ -95,7 +95,6 @@ m4_foreach_w([_m], [
HTTP::Request HTTP::Request
HTTP::Response HTTP::Response
JSON::PP JSON::PP
LWP::UserAgent
Test::MockModule Test::MockModule
Test::TCP Test::TCP
Test::Warnings Test::Warnings

View file

@ -131,7 +131,6 @@ my $daemon_default = ($programd =~ /d$/) ? interval('5m') : undef;
# Current Logger instance. To push a context prefix onto the context stack: # Current Logger instance. To push a context prefix onto the context stack:
# local _l = pushlogctx('additional context goes here'); # local _l = pushlogctx('additional context goes here');
our $_l = ddclient::Logger->new(); our $_l = ddclient::Logger->new();
our @_test_headers;
$ENV{'PATH'} = (exists($ENV{PATH}) ? "$ENV{PATH}:" : "") . "/sbin:/usr/sbin:/bin:/usr/bin:/etc:/usr/lib:"; $ENV{'PATH'} = (exists($ENV{PATH}) ? "$ENV{PATH}:" : "") . "/sbin:/usr/sbin:/bin:/usr/bin:/etc:/usr/lib:";
@ -2818,7 +2817,7 @@ sub geturl {
push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password)); push(@curlopt, "user=\"".escape_curl_param("${login}:${password}").'"') if (defined($login) && defined($password));
push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy); push(@curlopt, "proxy=\"".escape_curl_param("${protocol}://${proxy}").'"') if defined($proxy);
push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"'); push(@curlopt, "url=\"".escape_curl_param("${protocol}://${server}/${url}").'"');
push(@curlopt, map('header="' . escape_curl_param($_) . '"', @_test_headers, push(@curlopt, map('header="' . escape_curl_param($_) . '"',
ref($headers) eq 'ARRAY' ? @$headers : split('\n', $headers))); ref($headers) eq 'ARRAY' ? @$headers : split('\n', $headers)));
# Add in the data if any was provided (for POST/PATCH) # Add in the data if any was provided (for POST/PATCH)

View file

@ -1,6 +1,6 @@
use Test::More; use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
my $got_host; my $got_host;
my $builtinfw = 't/builtinfw_query.pl'; my $builtinfw = 't/builtinfw_query.pl';

View file

@ -1,12 +1,7 @@
use Test::More; use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use ddclient::t; use ddclient::t;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
#STDOUT->autoflush(1);
#$ddclient::globals{'debug'} = 1;
subtest "get_default_interface tests" => sub { subtest "get_default_interface tests" => sub {
for my $sample (@ddclient::t::routing_samples) { for my $sample (@ddclient::t::routing_samples) {

View file

@ -1,57 +1,22 @@
use Test::More; use Test::More;
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN {
my $has_http_daemon_ssl = eval { require HTTP::Daemon::SSL; }; eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
my $ipv6_supported = eval { ddclient::t::HTTPD->import();
require IO::Socket::IP;
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
# To aid in debugging, uncomment the following lines. (They are normally left commented to avoid
# accidentally interfering with the Test Anything Protocol messages written by Test::More.)
#STDOUT->autoflush(1);
#$ddclient::globals{'verbose'} = 1;
my $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
$ddclient::globals{'ssl_ca_file'} = "$certdir/dummy-ca-cert.pem";
sub run_httpd {
my ($ipv6, $ssl) = @_;
return undef if $ssl && !$has_http_daemon_ssl;
return undef if $ipv6 && (!$ipv6_supported || !$http_daemon_supports_ipv6);
my $httpd = ddclient::Test::Fake::HTTPD->new(
host => $ipv6 ? '::1' : '127.0.0.1',
scheme => $ssl ? 'https' : 'http',
daemon_args => {
SSL_cert_file => "$certdir/dummy-server-cert.pem",
SSL_key_file => "$certdir/dummy-server-key.pem",
V6Only => 1,
},
);
$httpd->run(sub {
# Echo back the full request.
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
});
diag(sprintf("started IPv%s%s server running at %s",
$ipv6 ? '6' : '4', $ssl ? ' SSL' : '', $httpd->endpoint()));
return $httpd;
} }
use ddclient::t::ip;
my %httpd = ( $ddclient::globals{'ssl_ca_file'} = $ca_file;
'4' => {'http' => run_httpd(0, 0), 'https' => run_httpd(0, 1)},
'6' => {'http' => run_httpd(1, 0), 'https' => run_httpd(1, 1)}, for my $ipv ('4', '6') {
); for my $ssl (0, 1) {
my $httpd = httpd($ipv, $ssl) or next;
$httpd->run(sub {
return [200, ['Content-Type' => 'application/octet-stream'], [$_[0]->as_string()]];
});
}
}
my @test_cases = ( my @test_cases = (
{ipv6_opt => 0, server_ipv => '4', client_ipv => ''}, {ipv6_opt => 0, server_ipv => '4', client_ipv => ''},
@ -79,9 +44,9 @@ for my $tc (@test_cases) {
skip("IPv6 not supported on this system", 1) skip("IPv6 not supported on this system", 1)
if $tc->{server_ipv} eq '6' && !$ipv6_supported; if $tc->{server_ipv} eq '6' && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1)
if $tc->{server_ipv} eq '6' && !$http_daemon_supports_ipv6; if $tc->{server_ipv} eq '6' && !$httpd_ipv6_supported;
skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$has_http_daemon_ssl; skip("HTTP::Daemon::SSL not available", 1) if $tc->{ssl} && !$httpd_ssl_supported;
my $uri = $httpd{$tc->{server_ipv}}{$tc->{ssl} ? 'https' : 'http'}->endpoint(); my $uri = httpd($tc->{server_ipv}, $tc->{ssl})->endpoint();
my $name = sprintf("IPv%s client to %s%s", my $name = sprintf("IPv%s client to %s%s",
$tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : ''); $tc->{client_ipv} || '*', $uri, $tc->{ipv6_opt} ? ' (-ipv6)' : '');
$ddclient::globals{'ipv6'} = $tc->{ipv6_opt}; $ddclient::globals{'ipv6'} = $tc->{ipv6_opt};

View file

@ -560,3 +560,5 @@ EOF
want_ipv6_if => "en0", want_ipv6_if => "en0",
}, },
); );
1;

135
t/lib/ddclient/t/HTTPD.pm Normal file
View file

@ -0,0 +1,135 @@
package ddclient::t::HTTPD;
use v5.10.1;
use strict;
use warnings;
use parent qw(ddclient::Test::Fake::HTTPD);
use Exporter qw(import);
use JSON::PP;
use Test::More;
BEGIN { require 'ddclient'; }
use ddclient::t::ip;
our @EXPORT = qw(
httpd
httpd_ipv6_ok httpd_ipv6_required $httpd_ipv6_supported $httpd_ipv6_support_error
httpd_ssl_ok httpd_ssl_required $httpd_ssl_supported $httpd_ssl_support_error
$ca_file $certdir
$textplain
);
our $httpd_ssl_support_error;
our $httpd_ssl_supported = eval { require HTTP::Daemon::SSL; 1; } or $httpd_ssl_support_error = $@;
sub httpd_ssl_ok {
ok($httpd_ssl_supported, "SSL is supported") or diag($httpd_ssl_support_error);
}
sub httpd_ssl_required {
plan(skip_all => $httpd_ssl_support_error) if !$httpd_ssl_supported;
}
our $httpd_ipv6_support_error;
our $httpd_ipv6_supported = $ipv6_supported or $httpd_ipv6_support_error = $ipv6_support_error;
$httpd_ipv6_supported = eval { require HTTP::Daemon; HTTP::Daemon->VERSION(6.12); }
or $httpd_ipv6_support_error = $@
if $httpd_ipv6_supported;
sub httpd_ipv6_ok {
ok($httpd_ipv6_supported, "test HTTP server supports IPv6") or diag($httpd_ipv6_support_error);
}
sub httpd_ipv6_required {
plan(skip_all => $httpd_ipv6_support_error) if !$httpd_ipv6_supported;
}
our $textplain = ['content-type' => 'text/plain; charset=utf-8'];
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
$self->{_requests} = []; # Log of received requests.
$self->{_responses} = []; # Script of responses to play back.
return $self;
}
sub run {
my ($self, $app) = @_;
$self->SUPER::run(sub {
my ($req) = @_;
push(@{$self->{_requests}}, $req);
my $res = $app->($req) if defined($app);
return $res if defined($res);
if ($req->uri()->path() eq '/control') {
pop(@{$self->{_requests}});
if ($req->method() eq 'PUT') {
return [400, $textplain, ['content must be json']]
if $req->headers()->content_type() ne 'application/json';
eval { @{$self->{_responses}} = @{decode_json($req->content())}; 1; }
or return [400, $textplain, ['content is not valid json']];
@{$self->{_requests}} = ();
return [200, $textplain, ["successfully reset request log and response script"]];
} elsif ($req->method() eq 'GET') {
my @reqs = map($_->as_string(), @{$self->{_requests}});
return [200, ['content-type' => 'application/json'], [encode_json(\@reqs)]];
} else {
return [405, $textplain, ['unsupported method: ' . $req->method()]];
}
}
return shift(@{$self->{_responses}}) // [500, $textplain, ["no more scripted responses"]];
});
diag("started server running at " . $self->endpoint());
return $self;
}
sub reset {
my $self = shift;
my $ep = $self->endpoint();
my $got = ddclient::geturl(url => "$ep/control");
diag("http response:\n$got");
ddclient::header_ok($got)
or BAIL_OUT("failed to get log of requests from test http server at $ep");
$got =~ s/^.*?\n\n//s;
my @got = map(HTTP::Request->parse($_), @{decode_json($got)});
ddclient::header_ok(ddclient::geturl(
url => "$ep/control",
method => 'PUT',
headers => ['content-type: application/json'],
data => encode_json(\@_),
)) or BAIL_OUT("failed to reset the test http server at $ep");
return @got;
}
our $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
our $ca_file = "$certdir/dummy-ca-cert.pem";
my %daemons;
sub httpd {
my ($ipv, $ssl) = @_;
$ipv //= '';
$ssl = !!$ssl;
return undef if $ipv eq '6' && !$httpd_ipv6_supported;
return undef if $ssl && !$httpd_ssl_supported;
if (!defined($daemons{$ipv}{$ssl})) {
my $host
= $ipv eq '4' ? '127.0.0.1'
: $ipv eq '6' ? '::1'
: $httpd_ipv6_supported ? '::1'
: '127.0.0.1';
$daemons{$ipv}{$ssl} = __PACKAGE__->new(
host => $host,
scheme => $ssl ? 'https' : 'http',
daemon_args => {
(V6Only => $ipv eq '6' ? 1 : 0) x ($host eq '::1'),
(SSL_cert_file => "$certdir/dummy-server-cert.pem",
SSL_key_file => "$certdir/dummy-server-key.pem") x $ssl,
},
);
}
return $daemons{$ipv}{$ssl};
}
1;

30
t/lib/ddclient/t/ip.pm Normal file
View file

@ -0,0 +1,30 @@
package ddclient::t::ip;
use v5.10.1;
use strict;
use warnings;
use Exporter qw(import);
use Test::More;
our @EXPORT = qw(ipv6_ok ipv6_required $ipv6_supported $ipv6_support_error);
our $ipv6_support_error;
our $ipv6_supported = eval {
require IO::Socket::IP;
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
} or $ipv6_support_error = $@;
sub ipv6_ok {
ok($ipv6_supported, "system supports IPv6") or diag($ipv6_support_error);
}
sub ipv6_required {
plan(skip_all => $ipv6_support_error) if !$ipv6_supported;
}
1;

View file

@ -1,14 +1,15 @@
use Test::More; use Test::More;
eval { require JSON::PP; } or plan(skip_all => $@); BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
JSON::PP->import(qw(encode_json)); BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN {
eval { require 'ddclient'; } or BAIL_OUT($@); eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
ddclient::load_json_support('directnic'); ddclient::load_json_support('directnic');
my $httpd = ddclient::Test::Fake::HTTPD->new(); httpd()->run(sub {
$httpd->run(sub {
my ($req) = @_; my ($req) = @_;
diag('=============================================================================='); diag('==============================================================================');
diag("Test server received request:\n" . $req->as_string()); diag("Test server received request:\n" . $req->as_string());
@ -28,11 +29,10 @@ $httpd->run(sub {
} }
return [400, $headers, ['unexpected request: ' . $req->uri()]] return [400, $headers, ['unexpected request: ' . $req->uri()]]
}); });
diag("started IPv4 HTTP server running at " . $httpd->endpoint());
{ {
package Logger; package Logger;
BEGIN { push(our @ISA, qw(ddclient::Logger)); } use parent qw(-norequire ddclient::Logger);
sub new { sub new {
my ($class, $parent) = @_; my ($class, $parent) = @_;
my $self = $class->SUPER::new(undef, $parent); my $self = $class->SUPER::new(undef, $parent);
@ -47,7 +47,7 @@ diag("started IPv4 HTTP server running at " . $httpd->endpoint());
} }
} }
my $hostname = $httpd->endpoint(); my $hostname = httpd()->endpoint();
my @test_cases = ( my @test_cases = (
{ {
desc => 'IPv4, good', desc => 'IPv4, good',

View file

@ -1,49 +1,25 @@
use Test::More; use Test::More;
eval { require JSON::PP; } or plan(skip_all => $@); BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
JSON::PP->import(qw(encode_json decode_json)); BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); BEGIN {
eval { require LWP::UserAgent; } or plan(skip_all => $@); eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
ddclient::load_json_support('dnsexit2'); ddclient::load_json_support('dnsexit2');
my @requests; # Declare global variable to store requests, used for tests. httpd()->run(sub {
my @httpd_requests; # Declare variable specificly used for the httpd process (which cannot be shared with tests).
my $httpd = ddclient::Test::Fake::HTTPD->new();
$httpd->run(sub {
my ($req) = @_; my ($req) = @_;
if ($req->uri->as_string eq '/get_requests') { return undef if $req->uri()->path() eq '/control';
return [200, ['Content-Type' => 'application/json'], [encode_json(\@httpd_requests)]];
} elsif ($req->uri->as_string eq '/reset_requests') {
@httpd_requests = ();
return [200, ['Content-Type' => 'application/json'], [encode_json({ message => 'OK' })]];
}
my $request_info = {
method => $req->method,
uri => $req->uri->as_string,
content => $req->content,
headers => $req->headers->as_string
};
push @httpd_requests, $request_info;
return [200, ['Content-Type' => 'application/json'], [encode_json({ return [200, ['Content-Type' => 'application/json'], [encode_json({
code => 0, code => 0,
message => 'Success' message => 'Success'
})]]; })]];
}); });
diag(sprintf("started IPv4 server running at %s", $httpd->endpoint()));
local $ddclient::globals{verbose} = 1; local $ddclient::globals{verbose} = 1;
my $ua = LWP::UserAgent->new;
sub test_nic_dnsexit2_update {
my ($config, @hostnames) = @_;
%ddclient::config = %$config;
ddclient::nic_dnsexit2_update(undef, @hostnames);
}
sub decode_and_sort_array { sub decode_and_sort_array {
my ($data) = @_; my ($data) = @_;
if (!ref $data) { if (!ref $data) {
@ -53,145 +29,132 @@ sub decode_and_sort_array {
return $data; return $data;
} }
sub reset_test_data {
my $response = $ua->get($httpd->endpoint . '/reset_requests');
die "Failed to reset requests" unless $response->is_success;
@requests = ();
}
sub get_requests {
my $res = $ua->get($httpd->endpoint . '/get_requests');
die "Failed to get requests: " . $res->status_line unless $res->is_success;
return @{decode_json($res->decoded_content)};
}
subtest 'Testing nic_dnsexit2_update' => sub { subtest 'Testing nic_dnsexit2_update' => sub {
my %config = ( httpd()->reset();
'host.my.zone.com' => { local %ddclient::config = (
'host.my.example.com' => {
'usev4' => 'ipv4', 'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4', 'wantipv4' => '192.0.2.1',
'usev6' => 'ipv6', 'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888', 'wantipv6' => '2001:db8::1',
'protocol' => 'dnsexit2', 'protocol' => 'dnsexit2',
'password' => 'mytestingpassword', 'password' => 'mytestingpassword',
'zone' => 'my.zone.com', 'zone' => 'my.example.com',
'server' => $httpd->endpoint(), 'server' => httpd()->endpoint(),
'path' => '/update', 'path' => '/update',
'ttl' => 5 'ttl' => 5
}); });
test_nic_dnsexit2_update(\%config, 'host.my.zone.com'); ddclient::nic_dnsexit2_update(undef, 'host.my.example.com');
@requests = get_requests(); my @requests = httpd()->reset();
is($requests[0]->{method}, 'POST', 'Method is correct'); is(scalar(@requests), 1, 'expected number of update requests');
is($requests[0]->{uri}, '/update', 'URI contains correct path'); my $req = shift(@requests);
like($requests[0]->{headers}, qr/Content-Type: application\/json/, 'Content-Type header is correct'); is($req->method(), 'POST', 'Method is correct');
like($requests[0]->{headers}, qr/Accept: application\/json/, 'Accept header is correct'); is($req->uri()->as_string(), '/update', 'URI contains correct path');
my $data = decode_and_sort_array($requests[0]->{content}); is($req->header('content-type'), 'application/json', 'Content-Type header is correct');
my $expected_data = decode_and_sort_array({ is($req->header('accept'), 'application/json', 'Accept header is correct');
'domain' => 'my.zone.com', my $got = decode_and_sort_array($req->content());
my $want = decode_and_sort_array({
'domain' => 'my.example.com',
'apikey' => 'mytestingpassword', 'apikey' => 'mytestingpassword',
'update' => [ 'update' => [
{ {
'type' => 'A', 'type' => 'A',
'name' => 'host', 'name' => 'host',
'content' => '8.8.4.4', 'content' => '192.0.2.1',
'ttl' => 5, 'ttl' => 5,
}, },
{ {
'type' => 'AAAA', 'type' => 'AAAA',
'name' => 'host', 'name' => 'host',
'content' => '2001:4860:4860::8888', 'content' => '2001:db8::1',
'ttl' => 5, 'ttl' => 5,
} }
] ]
}); });
is_deeply($data, $expected_data, 'Data is correct'); is_deeply($got, $want, 'Data is correct');
reset_test_data();
}; };
subtest 'Testing nic_dnsexit2_update without a zone set' => sub { subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
my %config = ( httpd()->reset();
'myhost.zone.com' => { local %ddclient::config = (
'myhost.example.com' => {
'usev4' => 'ipv4', 'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4', 'wantipv4' => '192.0.2.1',
'protocol' => 'dnsexit2', 'protocol' => 'dnsexit2',
'password' => 'anotherpassword', 'password' => 'anotherpassword',
'server' => $httpd->endpoint(), 'server' => httpd()->endpoint(),
'path' => '/update-alt', 'path' => '/update-alt',
'ttl' => 10 'ttl' => 10
}); });
test_nic_dnsexit2_update(\%config, 'myhost.zone.com'); ddclient::nic_dnsexit2_update(undef, 'myhost.example.com');
@requests = get_requests(); my @requests = httpd()->reset();
my $data = decode_and_sort_array($requests[0]->{content}); is(scalar(@requests), 1, 'expected number of update requests');
my $expected_data = decode_and_sort_array({ my $req = shift(@requests);
'domain' => 'myhost.zone.com', my $got = decode_and_sort_array($req->content());
my $want = decode_and_sort_array({
'domain' => 'myhost.example.com',
'apikey' => 'anotherpassword', 'apikey' => 'anotherpassword',
'update' => [ 'update' => [
{ {
'type' => 'A', 'type' => 'A',
'name' => '', 'name' => '',
'content' => '8.8.4.4', 'content' => '192.0.2.1',
'ttl' => 10, 'ttl' => 10,
} }
] ]
}); });
is_deeply($data, $expected_data, 'Data is correct'); is_deeply($got, $want, 'Data is correct');
reset_test_data($ua);
}; };
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub { subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
my %config = ( httpd()->reset();
'host1.zone.com' => { local %ddclient::config = (
'host1.example.com' => {
'usev4' => 'ipv4', 'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4', 'wantipv4' => '192.0.2.1',
'protocol' => 'dnsexit2', 'protocol' => 'dnsexit2',
'password' => 'testingpassword', 'password' => 'testingpassword',
'server' => $httpd->endpoint(), 'server' => httpd()->endpoint(),
'path' => '/update', 'path' => '/update',
'ttl' => 5 'ttl' => 5
}, },
'host2.zone.com' => { 'host2.example.com' => {
'usev6' => 'ipv6', 'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888', 'wantipv6' => '2001:db8::1',
'protocol' => 'dnsexit2', 'protocol' => 'dnsexit2',
'password' => 'testingpassword', 'password' => 'testingpassword',
'server' => $httpd->endpoint(), 'server' => httpd()->endpoint(),
'path' => '/update', 'path' => '/update',
'ttl' => 10, 'ttl' => 10,
'zone' => 'zone.com' 'zone' => 'example.com'
} }
); );
test_nic_dnsexit2_update(\%config, 'host1.zone.com', 'host2.zone.com'); ddclient::nic_dnsexit2_update(undef, 'host1.example.com', 'host2.example.com');
my $expected_data1 = decode_and_sort_array({ my @requests = httpd()->reset();
'domain' => 'host1.zone.com', my @got = map(decode_and_sort_array($_->content()), @requests);
'apikey' => 'testingpassword', my @want = (
'update' => [ decode_and_sort_array({
{ 'domain' => 'host1.example.com',
'apikey' => 'testingpassword',
'update' => [{
'type' => 'A', 'type' => 'A',
'name' => '', 'name' => '',
'content' => '8.8.4.4', 'content' => '192.0.2.1',
'ttl' => 5, 'ttl' => 5,
} }],
] }),
}); decode_and_sort_array({
my $expected_data2 = decode_and_sort_array({ 'domain' => 'example.com',
'domain' => 'zone.com', 'apikey' => 'testingpassword',
'apikey' => 'testingpassword', 'update' => [{
'update' => [
{
'type' => 'AAAA', 'type' => 'AAAA',
'name' => 'host2', 'name' => 'host2',
'content' => '2001:4860:4860::8888', 'content' => '2001:db8::1',
'ttl' => 10, 'ttl' => 10,
} }],
] }),
}); );
@requests = get_requests(); is_deeply(\@got, \@want, 'data is correct');
for my $i (0..1) {
my $data = decode_and_sort_array($requests[$i]->{content});
is_deeply($data, $expected_data1, 'Data is correct for call host1') if $i == 0;
is_deeply($data, $expected_data2, 'Data is correct for call host2') if $i == 1;
}
reset_test_data();
}; };
done_testing(); done_testing();

View file

@ -1,30 +1,28 @@
use Test::More; use Test::More;
use Scalar::Util qw(blessed); BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use MIME::Base64; use MIME::Base64;
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); use Scalar::Util qw(blessed);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN {
eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
ddclient::t::HTTPD->import();
}
my $httpd = ddclient::Test::Fake::HTTPD->new(); httpd()->run(sub {
$httpd->run(sub {
my ($req) = @_; my ($req) = @_;
diag('=============================================================================='); diag('==============================================================================');
diag("Test server received request:\n" . $req->as_string()); diag("Test server received request:\n" . $req->as_string());
my $headers = ['content-type' => 'text/plain; charset=utf-8']; return undef if $req->uri()->path() eq '/control';
my $wantauthn = 'Basic ' . encode_base64('username:password', ''); my $wantauthn = 'Basic ' . encode_base64('username:password', '');
return [401, [@$headers, 'www-authenticate' => 'Basic realm="realm", charset="UTF-8"'], return [401, [@$textplain, 'www-authenticate' => 'Basic realm="realm", charset="UTF-8"'],
['authentication required']] if ($req->header('authorization') // '') ne $wantauthn; ['authentication required']] if ($req->header('authorization') // '') ne $wantauthn;
return [400, $headers, ['invalid method: ' . $req->method()]] if $req->method() ne 'GET'; return [400, $textplain, ['invalid method: ' . $req->method()]] if $req->method() ne 'GET';
return [400, $headers, ['unexpected request: ' . $req->uri() . "\n", return undef;
'want: ' . $req->header('want-req')]]
if $req->uri() ne $req->header('want-req');
return [200, $headers, [map("$_\n", $req->header('line'))]];
}); });
diag("started IPv4 HTTP server running at " . $httpd->endpoint());
{ {
package Logger; package Logger;
BEGIN { push(our @ISA, qw(ddclient::Logger)); } use parent qw(-norequire ddclient::Logger);
sub new { sub new {
my ($class, $parent) = @_; my ($class, $parent) = @_;
my $self = $class->SUPER::new(undef, $parent); my $self = $class->SUPER::new(undef, $parent);
@ -256,18 +254,20 @@ for my $tc (@test_cases) {
$ddclient::config{$_} = { $ddclient::config{$_} = {
login => 'username', login => 'username',
password => 'password', password => 'password',
server => $httpd->endpoint(), server => httpd()->endpoint(),
script => '/nic/update', script => '/nic/update',
%{$tc->{cfg}{$_}}, %{$tc->{cfg}{$_}},
} for keys(%{$tc->{cfg}}); } for keys(%{$tc->{cfg}});
httpd()->reset([200, $textplain, [map("$_\n", @{$tc->{resp}})]]);
{ {
local @ddclient::_test_headers = (
"want-req: /nic/update?$tc->{wantquery}",
map("line: $_", @{$tc->{resp}}),
);
local $ddclient::_l = $l; local $ddclient::_l = $l;
ddclient::nic_dyndns2_update(undef, sort(keys(%{$tc->{cfg}}))); ddclient::nic_dyndns2_update(undef, sort(keys(%{$tc->{cfg}})));
} }
my @requests = httpd()->reset();
is(scalar(@requests), 1, "$tc->{desc}: single update request");
my $req = shift(@requests);
is($req->uri()->path(), '/nic/update', "$tc->{desc}: request path");
is($req->uri()->query(), $tc->{wantquery}, "$tc->{desc}: request query");
is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap") is_deeply(\%ddclient::recap, $tc->{wantrecap}, "$tc->{desc}: recap")
or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}], or diag(ddclient::repr(Values => [\%ddclient::recap, $tc->{wantrecap}],
Names => ['*got', '*want'])); Names => ['*got', '*want']));

View file

@ -1,7 +1,7 @@
use Test::More; use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use File::Temp; use File::Temp;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require 'ddclient'; } or BAIL_OUT($@);
local $ddclient::globals{debug} = 1; local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1; local $ddclient::globals{verbose} = 1;

View file

@ -1,48 +1,25 @@
use Test::More; use Test::More;
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN {
my $ipv6_supported = eval { eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
require IO::Socket::IP; ddclient::t::HTTPD->import();
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
sub run_httpd {
my ($ipv6) = @_;
return undef if $ipv6 && (!$ipv6_supported || !$http_daemon_supports_ipv6);
my $httpd = ddclient::Test::Fake::HTTPD->new(
host => $ipv6 ? '::1' : '127.0.0.1',
scheme => 'http',
daemon_args => {V6Only => 1},
);
my $out = $ipv6 ? '::1 skip ::2' : '127.0.0.1 skip 127.0.0.2';
$httpd->run(sub {
return [200, ['Content-Type' => 'text/plain'], [$out]];
});
diag(sprintf("started IPv%s SSL server running at %s", $ipv6 ? '6' : '4', $httpd->endpoint()));
return $httpd;
} }
my %httpd = ( use ddclient::t::ip;
'4' => run_httpd(0),
'6' => run_httpd(1), httpd('4')->run(
); sub { return [200, ['Content-Type' => 'text/plain'], ['127.0.0.1 skip 127.0.0.2']]; });
httpd('6')->run(
sub { return [200, ['Content-Type' => 'text/plain'], ['::1 skip ::2']]; })
if httpd('6');
my $builtinwebv4 = 't/skip.pl webv4'; my $builtinwebv4 = 't/skip.pl webv4';
my $builtinwebv6 = 't/skip.pl webv6'; my $builtinwebv6 = 't/skip.pl webv6';
my $builtinfw = 't/skip.pl fw'; my $builtinfw = 't/skip.pl fw';
$ddclient::builtinweb{$builtinwebv4} = {'url' => $httpd{'4'}->endpoint(), 'skip' => 'skip'}; $ddclient::builtinweb{$builtinwebv4} = {'url' => httpd('4')->endpoint(), 'skip' => 'skip'};
$ddclient::builtinweb{$builtinwebv6} = {'url' => $httpd{'6'}->endpoint(), 'skip' => 'skip'} $ddclient::builtinweb{$builtinwebv6} = {'url' => httpd('6')->endpoint(), 'skip' => 'skip'}
if $httpd{'6'}; if httpd('6');
$ddclient::builtinfw{$builtinfw} = {name => 'test', skip => 'skip'}; $ddclient::builtinfw{$builtinfw} = {name => 'test', skip => 'skip'};
%ddclient::builtinfw if 0; # suppress spurious warning "Name used only once: possible typo" %ddclient::builtinfw if 0; # suppress spurious warning "Name used only once: possible typo"
@ -50,8 +27,7 @@ sub run_test_case {
my %tc = @_; my %tc = @_;
SKIP: { SKIP: {
skip("IPv6 not supported on this system", 1) if $tc{ipv6} && !$ipv6_supported; skip("IPv6 not supported on this system", 1) if $tc{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1) if $tc{ipv6} && !$httpd_ipv6_supported;
if $tc{ipv6} && !$http_daemon_supports_ipv6;
my $h = 't/skip.pl'; my $h = 't/skip.pl';
$ddclient::config{$h} = $tc{cfg}; $ddclient::config{$h} = $tc{cfg};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo" %ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
@ -127,7 +103,7 @@ subtest "use='$builtinfw'" => sub {
run_test_case( run_test_case(
desc => "fw-skip='' cancels built-in skip", desc => "fw-skip='' cancels built-in skip",
cfg => { cfg => {
'fw' => $httpd{'4'}->endpoint(), 'fw' => httpd('4')->endpoint(),
'fw-skip' => '', 'fw-skip' => '',
'use' => $builtinfw, 'use' => $builtinfw,
}, },
@ -136,7 +112,7 @@ subtest "use='$builtinfw'" => sub {
run_test_case( run_test_case(
desc => 'fw-skip=undef uses built-in skip', desc => 'fw-skip=undef uses built-in skip',
cfg => { cfg => {
'fw' => $httpd{'4'}->endpoint(), 'fw' => httpd('4')->endpoint(),
'fw-skip' => undef, 'fw-skip' => undef,
'use' => $builtinfw, 'use' => $builtinfw,
}, },
@ -147,7 +123,7 @@ subtest "usev4='$builtinfw'" => sub {
run_test_case( run_test_case(
desc => "fwv4-skip='' cancels built-in skip", desc => "fwv4-skip='' cancels built-in skip",
cfg => { cfg => {
'fwv4' => $httpd{'4'}->endpoint(), 'fwv4' => httpd('4')->endpoint(),
'fwv4-skip' => '', 'fwv4-skip' => '',
'usev4' => $builtinfw, 'usev4' => $builtinfw,
}, },
@ -156,7 +132,7 @@ subtest "usev4='$builtinfw'" => sub {
run_test_case( run_test_case(
desc => 'fwv4-skip=undef uses built-in skip', desc => 'fwv4-skip=undef uses built-in skip',
cfg => { cfg => {
'fwv4' => $httpd{'4'}->endpoint(), 'fwv4' => httpd('4')->endpoint(),
'fwv4-skip' => undef, 'fwv4-skip' => undef,
'usev4' => $builtinfw, 'usev4' => $builtinfw,
}, },

View file

@ -1,55 +1,23 @@
use Test::More; use Test::More;
eval { BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
require ddclient::Test::Fake::HTTPD; BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
require HTTP::Daemon::SSL; BEGIN {
} or plan(skip_all => $@); eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } ddclient::t::HTTPD->import();
eval { require 'ddclient'; } or BAIL_OUT($@); }
my $ipv6_supported = eval { use ddclient::t::ip;
require IO::Socket::IP;
my $ipv6_socket = IO::Socket::IP->new( httpd_ssl_required();
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
# Note: $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to "$certdir/dummy-ca-cert.pem" # Note: $ddclient::globals{'ssl_ca_file'} is intentionally NOT set to "$certdir/dummy-ca-cert.pem"
# so that we can test what happens when certificate validation fails. # so that we can test what happens when certificate validation fails.
my $certdir = "$ENV{abs_top_srcdir}/t/lib/ddclient/Test/Fake/HTTPD";
sub run_httpd { httpd('4', 1)->run(sub { return [200, $textplain, ['127.0.0.1']]; });
my ($ipv6) = @_; httpd('6', 1)->run(sub { return [200, $textplain, ['::1']]; }) if httpd('6', 1);
return undef if $ipv6 && (!$ipv6_supported || !$http_daemon_supports_ipv6);
my $addr = $ipv6 ? '::1' : '127.0.0.1';
my $httpd = ddclient::Test::Fake::HTTPD->new(
host => $addr,
scheme => 'https',
daemon_args => {
SSL_cert_file => "$certdir/dummy-server-cert.pem",
SSL_key_file => "$certdir/dummy-server-key.pem",
V6Only => 1,
},
);
$httpd->run(sub {
return [200, ['Content-Type' => 'text/plain'], [$addr]];
});
diag(sprintf("started IPv%s SSL server running at %s", $ipv6 ? '6' : '4', $httpd->endpoint()));
return $httpd;
}
my $h = 't/ssl-validate.pl'; my $h = 't/ssl-validate.pl';
my %httpd = (
'4' => run_httpd(0),
'6' => run_httpd(1),
);
my %ep = ( my %ep = (
'4' => $httpd{'4'}->endpoint(), '4' => httpd('4', 1)->endpoint(),
'6' => $httpd{'6'} ? $httpd{'6'}->endpoint() : undef, '6' => httpd('6', 1) ? httpd('6', 1)->endpoint() : undef,
); );
my @test_cases = ( my @test_cases = (
@ -104,8 +72,7 @@ my @test_cases = (
for my $tc (@test_cases) { for my $tc (@test_cases) {
SKIP: { SKIP: {
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported; skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
if $tc->{ipv6} && !$http_daemon_supports_ipv6;
$ddclient::config{$h} = $tc->{cfg}; $ddclient::config{$h} = $tc->{cfg};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo" %ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
is(ddclient::get_ipv4($tc->{cfg}{usev4}, $h), $tc->{want}, $tc->{desc}) is(ddclient::get_ipv4($tc->{cfg}{usev4}, $h), $tc->{want}, $tc->{desc})

View file

@ -1,44 +1,46 @@
use Test::More; use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use File::Temp; use File::Temp;
BEGIN { eval { require HTTP::Request; 1; } or plan(skip_all => $@); }
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
use List::Util qw(max); use List::Util qw(max);
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); use Scalar::Util qw(refaddr);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
eval { require 'ddclient'; } or BAIL_OUT($@); BEGIN {
my $ipv6_supported = eval { eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
require IO::Socket::IP; ddclient::t::HTTPD->import();
my $ipv6_socket = IO::Socket::IP->new(
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
sub run_httpd {
my ($ipv) = @_;
return undef if $ipv eq '6' && (!$ipv6_supported || !$http_daemon_supports_ipv6);
my $httpd = ddclient::Test::Fake::HTTPD->new(
host => $ipv eq '4' ? '127.0.0.1' : '::1',
daemon_args => {V6Only => 1},
);
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
$httpd->run(sub { return [200, ['content-type' => 'text/plain; charset=utf-8'], [$ip]]; });
diag("started IPv$ipv HTTP server running at " . $httpd->endpoint());
return $httpd;
} }
my %httpd = ( use ddclient::t::ip;
'4' => run_httpd('4'),
'6' => run_httpd('6'), httpd('4')->run();
); httpd('6')->run() if httpd('6');
local %ddclient::builtinweb = ( local %ddclient::builtinweb = (
v4 => {url => "" . $httpd{'4'}->endpoint()}, v4 => {url => "" . httpd('4')->endpoint()},
defined($httpd{'6'}) ? (v6 => {url => "" . $httpd{'6'}->endpoint()}) : (), defined(httpd('6')) ? (v6 => {url => "" . httpd('6')->endpoint()}) : (),
); );
# Sentinel value used by `mergecfg` that means "this hash entry should be deleted if it exists."
my $DOES_NOT_EXIST = [];
sub mergecfg {
my %ret;
for my $cfg (@_) {
next if !defined($cfg);
for my $h (keys(%$cfg)) {
if (refaddr($cfg->{$h}) == refaddr($DOES_NOT_EXIST)) {
delete($ret{$h});
next;
}
$ret{$h} = {%{$ret{$h} // {}}, %{$cfg->{$h}}};
for my $k (keys(%{$ret{$h}})) {
my $a = refaddr($ret{$h}{$k});
delete($ret{$h}{$k}) if defined($a) && $a == refaddr($DOES_NOT_EXIST);
}
}
}
return \%ret;
}
local $ddclient::globals{debug} = 1; local $ddclient::globals{debug} = 1;
local $ddclient::globals{verbose} = 1; local $ddclient::globals{verbose} = 1;
local $ddclient::now = 1000; local $ddclient::now = 1000;
@ -51,10 +53,10 @@ local %ddclient::protocols = (
update => sub { update => sub {
my $self = shift; my $self = shift;
ddclient::debug('in update'); ddclient::debug('in update');
push(@updates, [@_]);
for my $h (@_) { for my $h (@_) {
local $ddclient::_l = ddclient::pushlogctx($h); local $ddclient::_l = ddclient::pushlogctx($h);
ddclient::debug('updating host'); ddclient::debug('updating host');
push(@updates, [@_]);
$ddclient::recap{$h}{status} = 'good'; $ddclient::recap{$h}{status} = 'good';
$ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip}); $ddclient::recap{$h}{ip} = delete($ddclient::config{$h}{wantip});
$ddclient::recap{$h}{mtime} = $ddclient::now; $ddclient::recap{$h}{mtime} = $ddclient::now;
@ -70,82 +72,88 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, fresh, $desc", desc => "legacy, fresh, $desc",
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_update => 1, want_reqs_webv4 => 1,
want_recap_changes => { want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
{ {
desc => 'legacy, fresh, use=web (IPv6)', desc => 'legacy, fresh, use=web (IPv6)',
ipv6 => 1, ipv6 => 1,
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
'use' => 'web', 'use' => 'web',
'web' => 'v6', 'web' => 'v6',
}, }},
want_update => 1, want_reqs_webv6 => 1,
want_recap_changes => { want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv6' => '2001:db8::1', 'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
'status-ipv6' => 'good', 'status-ipv6' => 'good',
}, }},
}, },
{ {
desc => 'legacy, fresh, usev6=webv6', desc => 'legacy, fresh, usev6=webv6',
ipv6 => 1, ipv6 => 1,
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
'usev6' => 'webv6', 'usev6' => 'webv6',
}, }},
want_update => 1, want_reqs_webv6 => 1,
want_recap_changes => { want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv6' => '2001:db8::1', 'ipv6' => '2001:db8::1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
'status-ipv6' => 'good', 'status-ipv6' => 'good',
}, }},
}, },
{ {
desc => 'legacy, fresh, usev4=webv4 usev6=webv6', desc => 'legacy, fresh, usev4=webv4 usev6=webv6',
ipv6 => 1, ipv6 => 1,
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
'usev4' => 'webv4', 'usev4' => 'webv4',
'usev6' => 'webv6', 'usev6' => 'webv6',
}, }},
want_update => 1, want_reqs_webv4 => 1,
want_recap_changes => { want_reqs_webv6 => 1,
want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
}, },
map({ map({
my %cfg = %{delete($_->{cfg})}; my %cfg = %{delete($_->{cfg})};
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, no change, not yet time, $desc", desc => "legacy, no change, not yet time, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval'), 'atime' => $ddclient::now - ddclient::opt('min-interval'),
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now - ddclient::opt('min-interval'), 'mtime' => $ddclient::now - ddclient::opt('min-interval'),
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_reqs_webv4 => 1,
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -154,16 +162,17 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, min-interval elapsed but no change, $desc", desc => "legacy, min-interval elapsed but no change, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1, 'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1, 'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_reqs_webv4 => 1,
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -172,19 +181,20 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, needs update, not yet time, $desc", desc => "legacy, needs update, not yet time, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval'), 'atime' => $ddclient::now - ddclient::opt('min-interval'),
'ipv4' => '192.0.2.2', 'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-interval'), 'mtime' => $ddclient::now - ddclient::opt('min-interval'),
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_recap_changes => { want_reqs_webv4 => 1,
want_recap_changes => {host => {
'warned-min-interval' => $ddclient::now, 'warned-min-interval' => $ddclient::now,
}, }},
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -193,22 +203,23 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, min-interval elapsed, needs update, $desc", desc => "legacy, min-interval elapsed, needs update, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-interval') - 1, 'atime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'ipv4' => '192.0.2.2', 'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1, 'mtime' => $ddclient::now - ddclient::opt('min-interval') - 1,
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_update => 1, want_reqs_webv4 => 1,
want_recap_changes => { want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
}, }},
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -217,20 +228,21 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, previous failed update, not yet time to retry, $desc", desc => "legacy, previous failed update, not yet time to retry, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-error-interval'), 'atime' => $ddclient::now - ddclient::opt('min-error-interval'),
'ipv4' => '192.0.2.2', 'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - max(ddclient::opt('min-error-interval'), 'mtime' => $ddclient::now - max(ddclient::opt('min-error-interval'),
ddclient::opt('min-interval')) - 1, ddclient::opt('min-interval')) - 1,
'status-ipv4' => 'failed', 'status-ipv4' => 'failed',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_recap_changes => { want_reqs_webv4 => 1,
want_recap_changes => {host => {
'warned-min-error-interval' => $ddclient::now, 'warned-min-error-interval' => $ddclient::now,
}, }},
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -239,23 +251,24 @@ my @test_cases = (
my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg))); my $desc = join(' ', map("$_=$cfg{$_}", keys(%cfg)));
{ {
desc => "legacy, previous failed update, time to retry, $desc", desc => "legacy, previous failed update, time to retry, $desc",
recap => { recap => {host => {
'atime' => $ddclient::now - ddclient::opt('min-error-interval') - 1, 'atime' => $ddclient::now - ddclient::opt('min-error-interval') - 1,
'ipv4' => '192.0.2.2', 'ipv4' => '192.0.2.2',
'mtime' => $ddclient::now - ddclient::opt('min-error-interval') - 2, 'mtime' => $ddclient::now - ddclient::opt('min-error-interval') - 2,
'status-ipv4' => 'failed', 'status-ipv4' => 'failed',
}, }},
cfg => { cfg => {host => {
'protocol' => 'legacy', 'protocol' => 'legacy',
%cfg, %cfg,
}, }},
want_update => 1, want_reqs_webv4 => 1,
want_recap_changes => { want_updates => [['host']],
want_recap_changes => {host => {
'atime' => $ddclient::now, 'atime' => $ddclient::now,
'ipv4' => '192.0.2.1', 'ipv4' => '192.0.2.1',
'mtime' => $ddclient::now, 'mtime' => $ddclient::now,
'status-ipv4' => 'good', 'status-ipv4' => 'good',
}, }},
%$_, %$_,
}; };
} {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}), } {cfg => {use => 'web'}}, {cfg => {usev4 => 'webv4'}}),
@ -264,46 +277,60 @@ my @test_cases = (
for my $tc (@test_cases) { for my $tc (@test_cases) {
SKIP: { SKIP: {
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported; skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
if $tc->{ipv6} && !$http_daemon_supports_ipv6;
subtest($tc->{desc} => sub { subtest($tc->{desc} => sub {
local $ddclient::_l = ddclient::pushlogctx($tc->{desc}); local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
# Copy %{$tc->{recap}} so that updates to $recap{$h} don't update %{$tc->{recap}}. for my $ipv ('4', '6') {
local %ddclient::recap = (host => {%{$tc->{recap} // {}}}); $tc->{"want_reqs_webv$ipv"} //= 0;
my $want = $tc->{"want_reqs_webv$ipv"};
next if !defined(httpd($ipv)) && $want == 0;
local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
my $ip = $ipv eq '4' ? '192.0.2.1' : '2001:db8::1';
httpd($ipv)->reset(([200, $textplain, [$ip]]) x $want);
}
$tc->{recap}{$_}{host} //= $_ for keys(%{$tc->{recap} // {}});
# Deep copy `%{$tc->{recap}}` so that updates to `%ddclient::recap` don't mutate it.
local %ddclient::recap = %{mergecfg($tc->{recap})};
my $cachef = File::Temp->new(); my $cachef = File::Temp->new();
# $cachef is an object that stringifies to a filename. # $cachef is an object that stringifies to a filename.
local $ddclient::globals{cache} = "$cachef"; local $ddclient::globals{cache} = "$cachef";
my %cfg = ( $tc->{cfg} = {map({
web => 'v4', ($_ => {
webv4 => 'v4', host => $_,
webv6 => 'v6', web => 'v4',
%{$tc->{cfg} // {}}, webv4 => 'v4',
); webv6 => 'v6',
# Copy %cfg so that updates to $config{$h} don't update %cfg. %{$tc->{cfg}{$_}},
local %ddclient::config = (host => {%cfg}); });
} keys(%{$tc->{cfg} // {}}))};
# Deep copy `%{$tc->{cfg}}` so that updates to `%ddclient::config` don't mutate it.
local %ddclient::config = %{mergecfg($tc->{cfg})};
local @updates; local @updates;
ddclient::update_nics(); ddclient::update_nics();
TODO: { for my $ipv ('4', '6') {
local $TODO = $tc->{want_update_TODO}; next if !defined(httpd($ipv));
is_deeply(\@updates, [(['host']) x ($tc->{want_update} ? 1 : 0)], local $ddclient::_l = ddclient::pushlogctx("IPv$ipv");
'got expected update'); my @gotreqs = httpd($ipv)->reset();
my $got = @gotreqs;
my $want = $tc->{"want_reqs_webv$ipv"};
is($got, $want, "number of requests to webv$ipv service");
} }
my %want_recap = (host => { TODO: {
%{$tc->{recap} // {}}, local $TODO = $tc->{want_updates_TODO};
%{$tc->{want_recap_changes} // {}}, is_deeply(\@updates, $tc->{want_updates} // [], 'got expected updates')
}); or diag(ddclient::repr(Values => [\@updates, $tc->{want_updates}],
Names => ['*got', '*want']));
}
my %want_recap = %{mergecfg($tc->{recap}, $tc->{want_recap_changes})};
TODO: { TODO: {
local $TODO = $tc->{want_recap_changes_TODO}; local $TODO = $tc->{want_recap_changes_TODO};
is_deeply(\%ddclient::recap, \%want_recap, 'recap matches') is_deeply(\%ddclient::recap, \%want_recap, 'recap matches')
or diag(ddclient::repr(Values => [\%ddclient::recap, \%want_recap], or diag(ddclient::repr(Values => [\%ddclient::recap, \%want_recap],
Names => ['*got', '*want'])); Names => ['*got', '*want']));
} }
my %want_cfg = (host => { my %want_cfg = %{mergecfg($tc->{cfg}, $tc->{want_cfg_changes})};
%cfg,
%{$tc->{want_cfg_changes} // {}},
});
TODO: { TODO: {
local $TODO = $tc->{want_cfg_changes_TODO}; local $TODO = $tc->{want_cfg_changes_TODO};
is_deeply(\%ddclient::config, \%want_cfg, 'config matches') is_deeply(\%ddclient::config, \%want_cfg, 'config matches')

View file

@ -1,51 +1,27 @@
use Test::More; use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
use Scalar::Util qw(blessed); use Scalar::Util qw(blessed);
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@); BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
SKIP: { eval { require Test::Warnings; } or skip($@, 1); } BEGIN {
eval { require 'ddclient'; } or BAIL_OUT($@); eval { require ddclient::t::HTTPD; 1; } or plan(skip_all => $@);
my $ipv6_supported = eval { ddclient::t::HTTPD->import();
require IO::Socket::IP; }
my $ipv6_socket = IO::Socket::IP->new( use ddclient::t::ip;
Domain => 'PF_INET6',
LocalHost => '::1',
Listen => 1,
);
defined($ipv6_socket);
};
my $http_daemon_supports_ipv6 = eval {
require HTTP::Daemon;
HTTP::Daemon->VERSION(6.12);
};
my $builtinweb = 't/use_web.pl builtinweb'; my $builtinweb = 't/use_web.pl builtinweb';
my $h = 't/use_web.pl hostname'; my $h = 't/use_web.pl hostname';
sub run_httpd { my $headers = [
my ($ipv) = @_; @$textplain,
return undef if $ipv eq '6' && (!$ipv6_supported || !$http_daemon_supports_ipv6); 'this-ipv4-should-be-ignored' => 'skip skip2 192.0.2.255',
my $httpd = ddclient::Test::Fake::HTTPD->new( 'this-ipv6-should-be-ignored' => 'skip skip2 2001:db8::ff',
host => $ipv eq '4' ? '127.0.0.1' : '::1', ];
daemon_args => {V6Only => 1}, httpd('4')->run(sub { return [200, $headers, ['192.0.2.1 skip 192.0.2.2 skip2 192.0.2.3']]; });
); httpd('6')->run(sub { return [200, $headers, ['2001:db8::1 skip 2001:db8::2 skip2 2001:db8::3']]; })
my $headers = [ if httpd('6');
'content-type' => 'text/plain',
'this-ipv4-should-be-ignored' => 'skip skip2 192.0.2.255',
'this-ipv6-should-be-ignored' => 'skip skip2 2001:db8::ff',
];
my $content = $ipv eq '4'
? '192.0.2.1 skip 192.0.2.2 skip2 192.0.2.3'
: '2001:db8::1 skip 2001:db8::2 skip2 2001:db8::3';
$httpd->run(sub { return [200, $headers, [$content]]; });
diag("started IPv$ipv server running at ${\($httpd->endpoint())}");
return $httpd;
}
my %httpd = (
'4' => run_httpd('4'),
'6' => run_httpd('6'),
);
my %ep = ( my %ep = (
'4' => $httpd{'4'}->endpoint(), '4' => httpd('4')->endpoint(),
'6' => $httpd{'6'} ? $httpd{'6'}->endpoint() : undef, '6' => httpd('6') ? httpd('6')->endpoint() : undef,
); );
my @test_cases; my @test_cases;
@ -110,8 +86,7 @@ for my $tc (@test_cases) {
$ddclient::config if 0; $ddclient::config if 0;
SKIP: { SKIP: {
skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported; skip("IPv6 not supported on this system", 1) if $tc->{ipv6} && !$ipv6_supported;
skip("HTTP::Daemon too old for IPv6 support", 1) skip("HTTP::Daemon too old for IPv6 support", 1) if $tc->{ipv6} && !$httpd_ipv6_supported;
if $tc->{ipv6} && !$http_daemon_supports_ipv6;
is(ddclient::get_ip($tc->{cfg}{use}, $h), $tc->{want}, $tc->{desc}) is(ddclient::get_ip($tc->{cfg}{use}, $h), $tc->{want}, $tc->{desc})
if $tc->{cfg}{use}; if $tc->{cfg}{use};
is(ddclient::get_ipv4($tc->{cfg}{usev4}, $h), $tc->{want}, $tc->{desc}) is(ddclient::get_ipv4($tc->{cfg}{usev4}, $h), $tc->{want}, $tc->{desc})

View file

@ -1,7 +1,7 @@
use Test::More; use Test::More;
BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
use re qw(is_regexp); use re qw(is_regexp);
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my %variable_collections = ( my %variable_collections = (
map({ ($_ => $ddclient::cfgvars{$_}) } grep($_ ne 'merged', keys(%ddclient::cfgvars))), map({ ($_ => $ddclient::cfgvars{$_}) } grep($_ ne 'merged', keys(%ddclient::cfgvars))),