Merge pull request #684 from rhansen/dnsexit2
dnsexit2: Update multiple hosts at a time when possible
This commit is contained in:
commit
41170b9c08
7 changed files with 331 additions and 214 deletions
|
@ -16,6 +16,11 @@ repository history](https://github.com/ddclient/ddclient/commits/main).
|
|||
special characters are preserved literally.
|
||||
[#766](https://github.com/ddclient/ddclient/pull/766)
|
||||
|
||||
### New features
|
||||
|
||||
* `dnsexit2`: Multiple hosts are updated in a single API call when possible.
|
||||
[#684](https://github.com/ddclient/ddclient/pull/684)
|
||||
|
||||
## 2025-01-07 v4.0.0-rc.2
|
||||
|
||||
### Breaking changes
|
||||
|
|
|
@ -160,5 +160,6 @@ EXTRA_DIST += $(handwritten_tests) \
|
|||
t/lib/ddclient/Test/Fake/HTTPD/other-ca-cert.pem \
|
||||
t/lib/ddclient/t.pm \
|
||||
t/lib/ddclient/t/HTTPD.pm \
|
||||
t/lib/ddclient/t/Logger.pm \
|
||||
t/lib/ddclient/t/ip.pm \
|
||||
t/lib/ok.pm
|
||||
|
|
115
ddclient.in
115
ddclient.in
|
@ -2510,9 +2510,11 @@ sub ynu {
|
|||
# provided (it is ignored if the `msg` keyword is present).
|
||||
sub log {
|
||||
my $self = shift;
|
||||
my %args = (@_ % 2 ? (msg => pop) : (), @_);
|
||||
my %args = (label => '', @_ % 2 ? (msg => pop) : (), @_);
|
||||
$args{ctx} = [$args{ctx} // ()] if ref($args{ctx}) eq '';
|
||||
return $self->_log(\%args);
|
||||
$self->_log(\%args);
|
||||
$self->_failed() if $args{label} eq 'FAILED';
|
||||
$self->_abort() if $args{label} eq 'FATAL';
|
||||
}
|
||||
|
||||
sub _log {
|
||||
|
@ -2521,10 +2523,11 @@ sub ynu {
|
|||
# the caller's arrayref (in case it is reused in a future call).
|
||||
$args->{ctx} = [@{$self->{ctx}}, @{$args->{ctx}}];
|
||||
return $self->{parent}->_log($args) if defined($self->{parent});
|
||||
return if $args->{label} eq 'DEBUG' && !ddclient::opt('debug');
|
||||
return if $args->{label} eq 'INFO' && !ddclient::opt('verbose');
|
||||
my $buffer = $args->{msg} // '';
|
||||
chomp($buffer);
|
||||
if (!$args->{raw}) {
|
||||
$args->{label} //= '';
|
||||
my $prefix = $args->{label} ne '' ? sprintf("%-8s ", $args->{label} . ':') : '';
|
||||
$prefix .= "[$_]" for @{$args->{ctx}};
|
||||
$prefix .= '> ' if $prefix;
|
||||
|
@ -2544,6 +2547,20 @@ sub ynu {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
sub _failed {
|
||||
my ($self) = @_;
|
||||
return $self->{parent}->_failed() if defined($self->{parent});
|
||||
$ddclient::result = 'FAILED';
|
||||
$ddclient::result if 0; # Suppress spurious "used only once: possible typo" warning.
|
||||
}
|
||||
|
||||
sub _abort {
|
||||
my ($self) = @_;
|
||||
return $self->{parent}->_abort() if defined($self->{parent});
|
||||
ddclient::sendmail();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
# Intended use:
|
||||
|
@ -2552,12 +2569,12 @@ sub pushlogctx { my ($ctx) = @_; return ddclient::Logger->new($ctx, $_l); }
|
|||
|
||||
sub logmsg { $_l->log(@_); }
|
||||
sub _logmsg_fmt { $_[0] eq 'ctx' ? (shift, shift) : (), (@_ > 1) ? sprintf(shift, @_) : shift; }
|
||||
sub info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)) if opt('verbose'); }
|
||||
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)) if opt('debug'); }
|
||||
sub info { logmsg(email => 1, label => 'INFO', _logmsg_fmt(@_)); }
|
||||
sub debug { logmsg( label => 'DEBUG', _logmsg_fmt(@_)); }
|
||||
sub warning { logmsg(email => 1, label => 'WARNING', _logmsg_fmt(@_)); }
|
||||
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); sendmail(); exit(1); }
|
||||
sub fatal { logmsg(email => 1, label => 'FATAL', _logmsg_fmt(@_)); }
|
||||
sub success { logmsg(email => 1, label => 'SUCCESS', _logmsg_fmt(@_)); }
|
||||
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); $result = 'FAILED'; }
|
||||
sub failed { logmsg(email => 1, label => 'FAILED', _logmsg_fmt(@_)); }
|
||||
|
||||
sub prettytime { return scalar(localtime(shift)); }
|
||||
|
||||
|
@ -4080,54 +4097,55 @@ EoEXAMPLE
|
|||
######################################################################
|
||||
sub nic_dnsexit2_update {
|
||||
my $self = shift;
|
||||
# The DNSExit API does not support updating hosts with different zones at the same time,
|
||||
# handling update per host.
|
||||
for my $h (@_) {
|
||||
$config{$h}{'zone'} = $h if !defined(opt('zone', $h));
|
||||
dnsexit2_update_host($h);
|
||||
}
|
||||
dnsexit2_update_hostgroup($_) for group_hosts_by(\@_, qw(password path server ssl zone));
|
||||
}
|
||||
|
||||
sub dnsexit2_update_host {
|
||||
my ($h) = @_;
|
||||
local $_l = pushlogctx($h);
|
||||
my $name = $h;
|
||||
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
|
||||
# set to the empty string; both have identical semantics. For consistency, always
|
||||
# remove the zone even if it means $name becomes the empty string.
|
||||
my $zone = opt('zone', $h);
|
||||
if ($name =~ s/(?:^|\.)\Q$zone\E$//) {
|
||||
# The zone was successfully trimmed from $name.
|
||||
} else {
|
||||
fatal("hostname does not end with the zone: " . opt('zone', $h));
|
||||
}
|
||||
# The IPv4 and IPv6 addresses must be updated together in a single API call.
|
||||
my %ips;
|
||||
sub dnsexit2_update_hostgroup {
|
||||
my ($group) = @_;
|
||||
return unless @{$group->{hosts}} > 0;
|
||||
local $_l = pushlogctx(join(', ', @{$group->{hosts}}));
|
||||
my %hostips;
|
||||
my @updates;
|
||||
for my $ipv ('4', '6') {
|
||||
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
|
||||
$ips{$ipv} = $ip;
|
||||
info("updating IPv$ipv address to $ip");
|
||||
$recap{$h}{"status-ipv$ipv"} = 'failed';
|
||||
push(@updates, {
|
||||
name => $name,
|
||||
type => ($ipv eq '6') ? 'AAAA' : 'A',
|
||||
content => $ip,
|
||||
ttl => opt('ttl', $h),
|
||||
});
|
||||
};
|
||||
my $url = opt('server', $h) . opt('path', $h);
|
||||
for my $h (@{$group->{hosts}}) {
|
||||
local $_l = pushlogctx($h) if @{$group->{hosts}} > 1;
|
||||
my $name = $h;
|
||||
# Remove the zone suffix from $name. If the zone eq $name, $name can be left alone or
|
||||
# set to the empty string; both have identical semantics. For consistency, always
|
||||
# remove the zone even if it means $name becomes the empty string.
|
||||
if ($name =~ s/(?:^|\.)\Q$group->{cfg}{'zone'}\E$//) {
|
||||
# The zone was successfully trimmed from $name.
|
||||
} else {
|
||||
fatal("hostname does not end with the zone: $group->{cfg}{'zone'}");
|
||||
}
|
||||
# The IPv4 and IPv6 addresses must be updated together in a single API call.
|
||||
for my $ipv ('4', '6') {
|
||||
my $ip = delete($config{$h}{"wantipv$ipv"}) or next;
|
||||
$hostips{$h}{$ipv} = $ip;
|
||||
info("updating IPv$ipv address to $ip");
|
||||
$recap{$h}{"status-ipv$ipv"} = 'failed';
|
||||
push(@updates, {
|
||||
name => $name,
|
||||
type => ($ipv eq '6') ? 'AAAA' : 'A',
|
||||
content => $ip,
|
||||
ttl => opt('ttl', $h),
|
||||
});
|
||||
}
|
||||
}
|
||||
return unless @updates > 0;
|
||||
my $reply = geturl(
|
||||
proxy => opt('proxy'),
|
||||
url => $url,
|
||||
url => $group->{cfg}{'server'} . $group->{cfg}{'path'},
|
||||
headers => [
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
],
|
||||
method => 'POST',
|
||||
data => encode_json({
|
||||
apikey => opt('password', $h),
|
||||
domain => $zone,
|
||||
apikey => $group->{cfg}{'password'},
|
||||
domain => $group->{cfg}{'zone'},
|
||||
update => \@updates,
|
||||
}),
|
||||
);
|
||||
|
@ -4174,12 +4192,15 @@ sub dnsexit2_update_host {
|
|||
return;
|
||||
}
|
||||
success($message);
|
||||
$recap{$h}{'mtime'} = $now;
|
||||
keys(%ips); # Reset internal iterator.
|
||||
while (my ($ipv, $ip) = each(%ips)) {
|
||||
$recap{$h}{"ipv$ipv"} = $ip;
|
||||
$recap{$h}{"status-ipv$ipv"} = 'good';
|
||||
success("updated IPv$ipv address to $ip");
|
||||
keys(%hostips); # Reset internal iterator.
|
||||
while (my ($h, $ips) = each(%hostips)) {
|
||||
$recap{$h}{'mtime'} = $now;
|
||||
keys(%$ips); # Reset internal iterator.
|
||||
while (my ($ipv, $ip) = each(%$ips)) {
|
||||
$recap{$h}{"ipv$ipv"} = $ip;
|
||||
$recap{$h}{"status-ipv$ipv"} = 'good';
|
||||
success("updated IPv$ipv address to $ip");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
39
t/lib/ddclient/t/Logger.pm
Normal file
39
t/lib/ddclient/t/Logger.pm
Normal file
|
@ -0,0 +1,39 @@
|
|||
package ddclient::t::Logger;
|
||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||
use parent qw(-norequire ddclient::Logger);
|
||||
|
||||
{
|
||||
package ddclient::t::LoggerAbort;
|
||||
use overload '""' => qw(stringify);
|
||||
sub new {
|
||||
my ($class, %args) = @_;
|
||||
return bless(\%args, $class);
|
||||
}
|
||||
sub stringify {
|
||||
return 'logged a FATAL message';
|
||||
}
|
||||
}
|
||||
|
||||
sub new {
|
||||
my ($class, $parent, $labelre) = @_;
|
||||
my $self = $class->SUPER::new(undef, $parent);
|
||||
$self->{logs} = [];
|
||||
$self->{_labelre} = $labelre;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub _log {
|
||||
my ($self, $args) = @_;
|
||||
my $lre = $self->{_labelre};
|
||||
my $lbl = $args->{label};
|
||||
push(@{$self->{logs}}, $args) if !defined($lre) || (defined($lbl) && $lbl =~ $lre);
|
||||
return $self->SUPER::_log($args);
|
||||
}
|
||||
|
||||
sub _abort {
|
||||
my ($self) = @_;
|
||||
push(@{$self->{logs}}, 'aborted');
|
||||
die(ddclient::t::LoggerAbort->new());
|
||||
}
|
||||
|
||||
1;
|
|
@ -3,6 +3,7 @@ BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
|||
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||
use ddclient::t::HTTPD;
|
||||
use ddclient::t::Logger;
|
||||
|
||||
httpd_required();
|
||||
|
||||
|
@ -29,23 +30,6 @@ httpd()->run(sub {
|
|||
return [400, $headers, ['unexpected request: ' . $req->uri()]]
|
||||
});
|
||||
|
||||
{
|
||||
package Logger;
|
||||
use parent qw(-norequire ddclient::Logger);
|
||||
sub new {
|
||||
my ($class, $parent) = @_;
|
||||
my $self = $class->SUPER::new(undef, $parent);
|
||||
$self->{logs} = [];
|
||||
return $self;
|
||||
}
|
||||
sub _log {
|
||||
my ($self, $args) = @_;
|
||||
push(@{$self->{logs}}, $args)
|
||||
if ($args->{label} // '') =~ qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/;
|
||||
return $self->SUPER::_log($args);
|
||||
}
|
||||
}
|
||||
|
||||
my $hostname = httpd()->endpoint();
|
||||
my @test_cases = (
|
||||
{
|
||||
|
@ -149,7 +133,7 @@ for my $tc (@test_cases) {
|
|||
diag('==============================================================================');
|
||||
local $ddclient::globals{debug} = 1;
|
||||
local $ddclient::globals{verbose} = 1;
|
||||
my $l = Logger->new($ddclient::_l);
|
||||
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||
local %ddclient::config = %{$tc->{cfg}};
|
||||
local %ddclient::recap;
|
||||
{
|
||||
|
|
|
@ -3,9 +3,13 @@ BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
|||
BEGIN { eval { require JSON::PP; 1; } or plan(skip_all => $@); JSON::PP->import(); }
|
||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||
use ddclient::t::HTTPD;
|
||||
use ddclient::t::Logger;
|
||||
|
||||
httpd_required();
|
||||
|
||||
local $ddclient::globals{debug} = 1;
|
||||
local $ddclient::globals{verbose} = 1;
|
||||
|
||||
ddclient::load_json_support('dnsexit2');
|
||||
|
||||
httpd()->run(sub {
|
||||
|
@ -17,143 +21,222 @@ httpd()->run(sub {
|
|||
})]];
|
||||
});
|
||||
|
||||
local $ddclient::globals{verbose} = 1;
|
||||
|
||||
sub decode_and_sort_array {
|
||||
my ($data) = @_;
|
||||
if (!ref $data) {
|
||||
$data = decode_json($data);
|
||||
}
|
||||
@{$data->{update}} = sort { $a->{type} cmp $b->{type} } @{$data->{update}};
|
||||
return $data;
|
||||
sub cmp_update {
|
||||
my ($a, $b) = @_;
|
||||
return $a->{name} cmp $b->{name} || $a->{type} cmp $b->{type};
|
||||
}
|
||||
|
||||
subtest 'Testing nic_dnsexit2_update' => sub {
|
||||
httpd()->reset();
|
||||
local %ddclient::config = (
|
||||
'host.my.example.com' => {
|
||||
'usev4' => 'ipv4',
|
||||
'wantipv4' => '192.0.2.1',
|
||||
'usev6' => 'ipv6',
|
||||
'wantipv6' => '2001:db8::1',
|
||||
'protocol' => 'dnsexit2',
|
||||
'password' => 'mytestingpassword',
|
||||
'zone' => 'my.example.com',
|
||||
'server' => httpd()->endpoint(),
|
||||
'path' => '/update',
|
||||
'ttl' => 5
|
||||
});
|
||||
ddclient::nic_dnsexit2_update(undef, 'host.my.example.com');
|
||||
my @requests = httpd()->reset();
|
||||
is(scalar(@requests), 1, 'expected number of update requests');
|
||||
my $req = shift(@requests);
|
||||
is($req->method(), 'POST', 'Method is correct');
|
||||
is($req->uri()->as_string(), '/update', 'URI contains correct path');
|
||||
is($req->header('content-type'), 'application/json', 'Content-Type header is correct');
|
||||
is($req->header('accept'), 'application/json', 'Accept header is correct');
|
||||
my $got = decode_and_sort_array($req->content());
|
||||
my $want = decode_and_sort_array({
|
||||
'domain' => 'my.example.com',
|
||||
'apikey' => 'mytestingpassword',
|
||||
'update' => [
|
||||
sub sort_updates {
|
||||
my ($req) = @_;
|
||||
return {
|
||||
%$req,
|
||||
update => [sort({ cmp_update($a, $b); } @{$req->{update}})],
|
||||
};
|
||||
}
|
||||
|
||||
sub sort_reqs {
|
||||
my @reqs = map(sort_updates($_), @_);
|
||||
my @sorted = sort({
|
||||
my $ret = $a->{domain} cmp $b->{domain};
|
||||
$ret = @{$a->{update}} <=> @{$b->{update}} if !$ret;
|
||||
my $i = 0;
|
||||
while (!$ret && $i < @{$a->{update}} && $i < @{$b->{update}}) {
|
||||
$ret = cmp_update($a->{update}[$i], $b->{update}[$i]);
|
||||
}
|
||||
return $ret;
|
||||
} @reqs);
|
||||
return @sorted;
|
||||
}
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
desc => 'both IPv4 and IPv6 are updated together',
|
||||
cfg => {
|
||||
'host.my.example.com' => {
|
||||
ttl => 5,
|
||||
wantipv4 => '192.0.2.1',
|
||||
wantipv6 => '2001:db8::1',
|
||||
zone => 'my.example.com',
|
||||
},
|
||||
},
|
||||
want => [{
|
||||
apikey => 'key',
|
||||
domain => 'my.example.com',
|
||||
update => [
|
||||
{
|
||||
content => '192.0.2.1',
|
||||
name => 'host',
|
||||
ttl => 5,
|
||||
type => 'A',
|
||||
},
|
||||
{
|
||||
content => '2001:db8::1',
|
||||
name => 'host',
|
||||
ttl => 5,
|
||||
type => 'AAAA',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
{
|
||||
desc => 'zone defaults to host',
|
||||
cfg => {
|
||||
'host.my.example.com' => {
|
||||
ttl => 10,
|
||||
wantipv4 => '192.0.2.1',
|
||||
},
|
||||
},
|
||||
want => [{
|
||||
apikey => 'key',
|
||||
domain => 'host.my.example.com',
|
||||
update => [
|
||||
{
|
||||
content => '192.0.2.1',
|
||||
name => '',
|
||||
ttl => 10,
|
||||
type => 'A',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
{
|
||||
desc => 'two hosts, different zones',
|
||||
cfg => {
|
||||
'host1.example.com' => {
|
||||
ttl => 5,
|
||||
wantipv4 => '192.0.2.1',
|
||||
# 'zone' intentionally not set, so it will default to 'host1.example.com'.
|
||||
},
|
||||
'host2.example.com' => {
|
||||
ttl => 10,
|
||||
wantipv6 => '2001:db8::1',
|
||||
zone => 'example.com',
|
||||
},
|
||||
},
|
||||
want => [
|
||||
{
|
||||
'type' => 'A',
|
||||
'name' => 'host',
|
||||
'content' => '192.0.2.1',
|
||||
'ttl' => 5,
|
||||
apikey => 'key',
|
||||
domain => 'host1.example.com',
|
||||
update => [
|
||||
{
|
||||
content => '192.0.2.1',
|
||||
name => '',
|
||||
ttl => 5,
|
||||
type => 'A',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type' => 'AAAA',
|
||||
'name' => 'host',
|
||||
'content' => '2001:db8::1',
|
||||
'ttl' => 5,
|
||||
}
|
||||
]
|
||||
});
|
||||
is_deeply($got, $want, 'Data is correct');
|
||||
};
|
||||
|
||||
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
|
||||
httpd()->reset();
|
||||
local %ddclient::config = (
|
||||
'myhost.example.com' => {
|
||||
'usev4' => 'ipv4',
|
||||
'wantipv4' => '192.0.2.1',
|
||||
'protocol' => 'dnsexit2',
|
||||
'password' => 'anotherpassword',
|
||||
'server' => httpd()->endpoint(),
|
||||
'path' => '/update-alt',
|
||||
'ttl' => 10
|
||||
});
|
||||
ddclient::nic_dnsexit2_update(undef, 'myhost.example.com');
|
||||
my @requests = httpd()->reset();
|
||||
is(scalar(@requests), 1, 'expected number of update requests');
|
||||
my $req = shift(@requests);
|
||||
my $got = decode_and_sort_array($req->content());
|
||||
my $want = decode_and_sort_array({
|
||||
'domain' => 'myhost.example.com',
|
||||
'apikey' => 'anotherpassword',
|
||||
'update' => [
|
||||
{
|
||||
'type' => 'A',
|
||||
'name' => '',
|
||||
'content' => '192.0.2.1',
|
||||
'ttl' => 10,
|
||||
}
|
||||
]
|
||||
});
|
||||
is_deeply($got, $want, 'Data is correct');
|
||||
};
|
||||
|
||||
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
|
||||
httpd()->reset();
|
||||
local %ddclient::config = (
|
||||
'host1.example.com' => {
|
||||
'usev4' => 'ipv4',
|
||||
'wantipv4' => '192.0.2.1',
|
||||
'protocol' => 'dnsexit2',
|
||||
'password' => 'testingpassword',
|
||||
'server' => httpd()->endpoint(),
|
||||
'path' => '/update',
|
||||
'ttl' => 5
|
||||
apikey => 'key',
|
||||
domain => 'example.com',
|
||||
update => [
|
||||
{
|
||||
content => '2001:db8::1',
|
||||
name => 'host2',
|
||||
ttl => 10,
|
||||
type => 'AAAA',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'two hosts, same zone',
|
||||
cfg => {
|
||||
'host1.example.com' => {
|
||||
ttl => 5,
|
||||
wantipv4 => '192.0.2.1',
|
||||
zone => 'example.com',
|
||||
},
|
||||
'host2.example.com' => {
|
||||
ttl => 10,
|
||||
wantipv6 => '2001:db8::1',
|
||||
zone => 'example.com',
|
||||
},
|
||||
},
|
||||
'host2.example.com' => {
|
||||
'usev6' => 'ipv6',
|
||||
'wantipv6' => '2001:db8::1',
|
||||
'protocol' => 'dnsexit2',
|
||||
'password' => 'testingpassword',
|
||||
'server' => httpd()->endpoint(),
|
||||
'path' => '/update',
|
||||
'ttl' => 10,
|
||||
'zone' => 'example.com'
|
||||
want => [
|
||||
{
|
||||
apikey => 'key',
|
||||
domain => 'example.com',
|
||||
update => [
|
||||
{
|
||||
content => '192.0.2.1',
|
||||
name => 'host1',
|
||||
ttl => 5,
|
||||
type => 'A',
|
||||
},
|
||||
{
|
||||
content => '2001:db8::1',
|
||||
name => 'host2',
|
||||
ttl => 10,
|
||||
type => 'AAAA',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
desc => 'host outside of zone',
|
||||
cfg => {
|
||||
'host.example' => {
|
||||
wantipv4 => '192.0.2.1',
|
||||
zone => 'example.com',
|
||||
},
|
||||
},
|
||||
want_fatal => qr{hostname does not end with the zone: example.com},
|
||||
},
|
||||
);
|
||||
|
||||
for my $tc (@test_cases) {
|
||||
subtest($tc->{desc} => sub {
|
||||
local $ddclient::_l = ddclient::pushlogctx($tc->{desc});
|
||||
local %ddclient::config = ();
|
||||
my @hosts = keys(%{$tc->{cfg}});
|
||||
for my $h (@hosts) {
|
||||
$ddclient::config{$h} = {
|
||||
password => 'key',
|
||||
path => '/update',
|
||||
server => httpd()->endpoint(),
|
||||
%{$tc->{cfg}{$h}},
|
||||
};
|
||||
}
|
||||
);
|
||||
ddclient::nic_dnsexit2_update(undef, 'host1.example.com', 'host2.example.com');
|
||||
my @requests = httpd()->reset();
|
||||
my @got = map(decode_and_sort_array($_->content()), @requests);
|
||||
my @want = (
|
||||
decode_and_sort_array({
|
||||
'domain' => 'host1.example.com',
|
||||
'apikey' => 'testingpassword',
|
||||
'update' => [{
|
||||
'type' => 'A',
|
||||
'name' => '',
|
||||
'content' => '192.0.2.1',
|
||||
'ttl' => 5,
|
||||
}],
|
||||
}),
|
||||
decode_and_sort_array({
|
||||
'domain' => 'example.com',
|
||||
'apikey' => 'testingpassword',
|
||||
'update' => [{
|
||||
'type' => 'AAAA',
|
||||
'name' => 'host2',
|
||||
'content' => '2001:db8::1',
|
||||
'ttl' => 10,
|
||||
}],
|
||||
}),
|
||||
);
|
||||
is_deeply(\@got, \@want, 'data is correct');
|
||||
};
|
||||
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^FATAL$/);
|
||||
my $err = do {
|
||||
local $ddclient::_l = $l;
|
||||
local $@;
|
||||
(eval { ddclient::nic_dnsexit2_update(undef, @hosts); 1; })
|
||||
? undef : ($@ // 'unknown error');
|
||||
};
|
||||
my @requests = httpd()->reset();
|
||||
my @got;
|
||||
for (my $i = 0; $i < @requests; $i++) {
|
||||
subtest("request $i" => sub {
|
||||
my $req = $requests[$i];
|
||||
is($req->method(), 'POST', 'method is POST');
|
||||
is($req->uri()->as_string(), '/update', 'path is /update');
|
||||
is($req->header('content-type'), 'application/json', 'Content-Type is JSON');
|
||||
is($req->header('accept'), 'application/json', 'Accept is JSON');
|
||||
my $got = decode_json($req->content());
|
||||
is(ref($got), 'HASH', 'request content is a JSON object');
|
||||
is(ref($got->{update}), 'ARRAY', 'JSON object has array "update" property');
|
||||
push(@got, $got);
|
||||
});
|
||||
}
|
||||
@got = sort_reqs(@got);
|
||||
my @want = sort_reqs(@{$tc->{want} // []});
|
||||
is_deeply(\@got, \@want, 'request objects match');
|
||||
subtest('expected (or lack of) error' => sub {
|
||||
if (is(defined($err), defined($tc->{want_fatal}), 'error existence') && defined($err)) {
|
||||
my @got = @{$l->{logs}};
|
||||
if (is(scalar(@got), 2, 'logged two events')) {
|
||||
is($got[0]->{label}, 'FATAL', 'first logged event is a FATAL message');
|
||||
like($got[0]->{msg}, $tc->{want_fatal}, 'first logged event message matches');
|
||||
is($got[1], 'aborted', 'second logged event is an "aborted" event');
|
||||
isa_ok($err, qw(ddclient::t::LoggerAbort));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
|
|
@ -3,6 +3,7 @@ BEGIN { SKIP: { eval { require Test::Warnings; 1; } or skip($@, 1); } }
|
|||
use MIME::Base64;
|
||||
BEGIN { eval { require 'ddclient'; } or BAIL_OUT($@); }
|
||||
use ddclient::t::HTTPD;
|
||||
use ddclient::t::Logger;
|
||||
|
||||
httpd_required();
|
||||
|
||||
|
@ -18,23 +19,6 @@ httpd()->run(sub {
|
|||
return undef;
|
||||
});
|
||||
|
||||
{
|
||||
package Logger;
|
||||
use parent qw(-norequire ddclient::Logger);
|
||||
sub new {
|
||||
my ($class, $parent) = @_;
|
||||
my $self = $class->SUPER::new(undef, $parent);
|
||||
$self->{logs} = [];
|
||||
return $self;
|
||||
}
|
||||
sub _log {
|
||||
my ($self, $args) = @_;
|
||||
push(@{$self->{logs}}, $args)
|
||||
if ($args->{label} // '') =~ qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/;
|
||||
return $self->SUPER::_log($args);
|
||||
}
|
||||
}
|
||||
|
||||
my @test_cases = (
|
||||
{
|
||||
desc => 'IPv4, single host, good',
|
||||
|
@ -246,7 +230,7 @@ for my $tc (@test_cases) {
|
|||
diag('==============================================================================');
|
||||
local $ddclient::globals{debug} = 1;
|
||||
local $ddclient::globals{verbose} = 1;
|
||||
my $l = Logger->new($ddclient::_l);
|
||||
my $l = ddclient::t::Logger->new($ddclient::_l, qr/^(?:WARNING|FATAL|SUCCESS|FAILED)$/);
|
||||
local %ddclient::config;
|
||||
local %ddclient::recap;
|
||||
$ddclient::config{$_} = {
|
||||
|
|
Loading…
Reference in a new issue