Merge remote-tracking branch 'upstream/master'

This commit is contained in:
philippderdiedas 2024-07-18 12:25:38 +00:00
commit f7419a052b
15 changed files with 2155 additions and 1480 deletions

View file

@ -33,6 +33,7 @@ jobs:
libtest-tcp-perl \
libtest-warnings-perl \
liburi-perl \
libwww-perl \
net-tools \
make \
;
@ -61,33 +62,28 @@ jobs:
- fedora:39
- fedora:latest
- fedora:rawhide
# RedHat UBI is mostly garbage due to a profound lack of basic
# packages. It is tested anyway because it's the closest available
# approximation of RHEL. Some of the packages needed for some tests
# aren't available, so those tests will be skipped. I guess it's
# still better than nothing.
- registry.access.redhat.com/ubi7/ubi:latest
- registry.access.redhat.com/ubi8/ubi:latest
- registry.access.redhat.com/ubi9/ubi:latest
- almalinux:8
- almalinux:latest
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
steps:
- if: ${{ matrix.image != 'registry.access.redhat.com/ubi7/ubi:latest' }}
uses: actions/checkout@v4
# ubi7 is too old for checkout@v4.
- if: ${{ matrix.image == 'registry.access.redhat.com/ubi7/ubi:latest' }}
uses: actions/checkout@v3
- name: install dependencies
# The --skip-broken argument works around RedHat UBI's missing packages.
# (They're only used for testing, so it's OK to not install them.)
- uses: actions/checkout@v4
- name: enable repositories (AlmaLinux 8)
if: ${{ matrix.image == 'almalinux:8' }}
run: |
inst="dnf --refresh --skip-broken install -y"
case '${{ matrix.image }}' in
# RedHat UBI 7 (RHEL 7) doesn't have dnf.
*ubi7*) inst="yum --skip-broken install -y";;
esac
${inst} \
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
dnf config-manager --set-enabled powertools
- name: enable repositories (AlmaLinux latest)
if: ${{ matrix.image == 'almalinux:latest' }}
run: |
dnf --refresh install -y 'dnf-command(config-manager)' epel-release &&
dnf config-manager --set-enabled crb
- name: install dependencies
# The --skip-broken argument works around missing packages. (They're
# only used for testing, so it's OK to not install them.)
run: |
dnf --refresh install --skip-broken -y \
automake \
findutils \
iproute \
@ -102,6 +98,7 @@ jobs:
perl-Test-TCP \
perl-Test-Warnings \
perl-core \
perl-libwww-perl \
net-tools \
;
- name: autogen

View file

@ -3,18 +3,37 @@
This document describes notable changes. For details, see the [source code
repository history](https://github.com/ddclient/ddclient/commits/master).
## v3.11.3~alpha (unreleased work-in-progress)
## v4.0.0~alpha (unreleased work-in-progress)
### Breaking changes
* The `--ssl` option is now enabled by default.
[#705](https://github.com/ddclient/ddclient/pull/705)
* Unencrypted (plain) HTTP is now used instead of encrypted (TLS) HTTP if the
URL uses `http://` instead of `https://`, even if the `--ssl` option is
enabled. [#608](https://github.com/ddclient/ddclient/pull/608)
* The `googledomains` built-in web IP discovery service
(`--webv4=googledomains`, `--webv6=googledomains`, and
`--web=googledomains`) is deprecated due to the service shutting down. It
will be removed in a future version of ddclient.
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
* The default web service for `--webv4` and `--webv6` has changed from Google
Domains (which is shutting down) to ipify.
[5b104ad1](https://github.com/ddclient/ddclient/commit/5b104ad116c023c3760129cab6e141f04f72b406)
* All log messages are now written to STDERR, not a mix of STDOUT and STDERR.
[#676](https://github.com/ddclient/ddclient/pull/676)
* For `--protocol=freedns` and `--protocol=nfsn`, the core module
`Digest::SHA` is now required. Previously, `Digest::SHA1` was used (if
available) as an alternative to `Digest::SHA`.
[#685](https://github.com/ddclient/ddclient/pull/685)
* The `he` built-in web IP discovery service (`--webv4=he`, `--webv6=he`, and
`--web=he`) was renamed to `he.net` for consistency with the new `he.net`
protocol. The old name is still accepted but is deprecated and will be
removed in a future version of ddclient.
[#682](https://github.com/ddclient/ddclient/pull/682)
* Deprecated built-in web IP discovery services are not listed in the output
of `--list-web-services`.
[#682](https://github.com/ddclient/ddclient/pull/682)
### New features
@ -41,6 +60,16 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
* The second and subsequent lines in a multi-line log message are now prefixed
with a `|` character.
[#676](https://github.com/ddclient/ddclient/pull/676)
* `emailonly`: New `protocol` option that simply emails you when your IP
address changes. [#654](https://github.com/ddclient/ddclient/pull/654)
* `he.net`: Added support for updating Hurricane Electric records.
[#682](https://github.com/ddclient/ddclient/pull/682)
* `dyndns2`, `domeneshop`, `dnsmadeeasy`, `keysystems`, `woima`: The `server`
option can now include `http://` or `https://` to control the use of TLS.
If omitted, the value of the `ssl` option is used to determine the scheme.
[#703](https://github.com/ddclient/ddclient/pull/703)
* `ddns.fm`: New `protocol` option for updating [DDNS.FM](https://ddns.fm/)
records. [#695](https://github.com/ddclient/ddclient/pull/695)
### Bug fixes
@ -73,6 +102,14 @@ repository history](https://github.com/ddclient/ddclient/commits/master).
[#667](https://github.com/ddclient/ddclient/pull/667)
* Fixed unnecessary repeated updates for some services.
[#670](https://github.com/ddclient/ddclient/pull/670)
* Fixed DNSExit provider when configured with a zone and non-identical
hostname. [#673](https://github.com/ddclient/ddclient/issues/673)
* `infomaniak`: Fixed frequent forced updates after 25 days (`max-interval`).
[#691](https://github.com/ddclient/ddclient/issues/691)
* `infomaniak`: Fixed incorrect parsing of server response.
[#692](https://github.com/ddclient/ddclient/issues/692)
* `regfishde`: Fixed IPv6 support.
[#691](https://github.com/ddclient/ddclient/issues/691)
## 2023-11-23 v3.11.2

View file

@ -63,8 +63,13 @@ AM_PL_LOG_FLAGS = -Mstrict -w \
-MDevel::Autoflush
handwritten_tests = \
t/builtinfw_query.pl \
t/dnsexit2.pl \
t/get_ip_from_if.pl \
t/geturl_connectivity.pl \
t/geturl_response.pl \
t/group_hosts_by.pl \
t/header_ok.pl \
t/interval_expired.pl \
t/is-and-extract-ipv4.pl \
t/is-and-extract-ipv6.pl \
t/is-and-extract-ipv6-global.pl \
@ -72,7 +77,8 @@ handwritten_tests = \
t/parse_assignments.pl \
t/skip.pl \
t/ssl-validate.pl \
t/write_cache.pl
t/variable_defaults.pl \
t/write_recap.pl
generated_tests = \
t/version.pl
TESTS = $(handwritten_tests) $(generated_tests)

View file

@ -17,11 +17,13 @@ Dynamic DNS services currently supported include:
* [ChangeIP](https://www.changeip.com)
* [CloudFlare](https://www.cloudflare.com)
* [ClouDNS](https://www.cloudns.net)
* [DDNS.fm](https://www.ddns.fm/)
* [DigitalOcean](https://www.digitalocean.com/)
* [dinahosting](https://dinahosting.com)
* [DonDominio](https://www.dondominio.com)
* [DNS Made Easy](https://dnsmadeeasy.com)
* [DNSExit](https://dnsexit.com/dns/dns-api)
* [dnsHome.de](https://www.dnshome.de)
* [Domeneshop](https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get)
* [DslReports](https://www.dslreports.com)
* [Duck DNS](https://duckdns.org)
@ -33,6 +35,7 @@ Dynamic DNS services currently supported include:
* [Gandi](https://gandi.net)
* [GoDaddy](https://www.godaddy.com)
* [Google](https://domains.google)
* [Hurricane Electric](https://dns.he.net)
* [Infomaniak](https://faq.infomaniak.com/2376)
* [Loopia](https://www.loopia.se)
* [Mythic Beasts](https://www.mythic-beasts.com/support/api/dnsv2/dynamic-dns)
@ -130,9 +133,16 @@ Note that any issues prior to version v3.9.1 will not be listed here.
If a fix is committed but not yet part of any tagged release, the notes here will reference the not-yet-released version number.
### v3.11.2 - v3.9.1: SSL parameter breaks HTTP-only IP acquisition
The `ssl` parameter forces all connections to use HTTPS. While technically working as expected, this behavior keeps coming up as a pain point when using HTTP-only IP querying sites such as http://checkip.dyndns.org. For the future (v3.11.3), the behavior is changed to respect `http://` in a URL. A separate parameter to disallow all HTTP connections or warn about them may be added later.
**Fix**: v3.11.3 will use HTTP to connect to URLs starting with `http://`. See [here](https://github.com/ddclient/ddclient/pull/608) for more info.
The `ssl` parameter forces all connections to use HTTPS. While technically
working as expected, this behavior keeps coming up as a pain point when using
HTTP-only IP querying sites such as http://checkip.dyndns.org. Starting with
v4.0.0, the behavior is changed to respect `http://` in a URL. A separate
parameter to disallow all HTTP connections or warn about them may be added
later.
**Fix**: v4.0.0 uses HTTP to connect to URLs starting with `http://`. See
[here](https://github.com/ddclient/ddclient/pull/608) for more info.
**Workaround**: Disable the SSL parameter

View file

@ -36,7 +36,18 @@ AC_PROG_MKDIR_P
AC_PATH_PROG([FIND], [find])
AS_IF([test -z "${FIND}"], [AC_MSG_ERROR(['find' utility not found])])
AC_PATH_PROG([CURL], [curl])
AC_ARG_WITH([curl],
[AS_HELP_STRING([[--with-curl[=CURL]]], [use CURL as absolute path to curl executable])],
[],
[with_curl=yes])
AS_CASE([${with_curl}],
[[yes]], [AC_PATH_PROG([CURL], [curl])],
[[no]], [CURL=],
[
AC_MSG_CHECKING([for curl])
CURL=${with_curl}
AC_MSG_RESULT([${CURL}])
]);
AS_IF([test -z "${CURL}"], [AC_MSG_ERROR([curl not found])])
AX_WITH_PROG([PERL], perl)
@ -49,6 +60,7 @@ AC_SUBST([PERL])
# package doesn't depend on all of them, so their availability can't
# be assumed.
m4_foreach_w([_m], [
Data::Dumper
File::Basename
File::Path
File::Temp
@ -63,7 +75,6 @@ m4_foreach_w([_m], [
# then some tests will fail. Only prints a warning if not installed.
m4_foreach_w([_m], [
B
Data::Dumper
File::Spec::Functions
File::Temp
], [AX_PROG_PERL_MODULES([_m], [],
@ -80,6 +91,8 @@ m4_foreach_w([_m], [
HTTP::Message::PSGI
HTTP::Request
HTTP::Response
JSON::PP
LWP::UserAgent
Scalar::Util
Test::MockModule
Test::TCP

View file

@ -16,13 +16,16 @@
## are mentioned here.
##
######################################################################
## Use encryption (TLS) when the scheme (either "http://" or "https://") is
## missing from a URL. Defaults to "yes".
#ssl=yes
daemon=300 # check every 300 seconds
syslog=yes # log update msgs to syslog
mail=root # mail all msgs to root
mail-failure=root # mail failed update msgs to root
pid=@runstatedir@/ddclient.pid # record PID in file.
ssl=yes # use ssl-support. Works with
# ssl-library
# postscript=script # run script after updating. The
# new IP is added as argument.
#
@ -222,6 +225,13 @@ ssl=yes # use ssl-support. Works with
# password=my-auto-generated-password
# my.domain.tld, otherhost.domain.tld
##
## Hurricane Electric (dns.he.net)
##
# protocol=he.net, \
# password=my-genereated-password \
# myhost.example.com
##
## Duckdns (http://www.duckdns.org/)
##
@ -238,6 +248,14 @@ ssl=yes # use ssl-support. Works with
# password=my-token
# myhost
##
## DDNS.FM (https://ddns.fm/)
##
#
# protocol=ddns.fm,
# password=my-token
# myhost.example.com
##
## MyOnlinePortal (http://myonlineportal.net)
##
@ -391,3 +409,18 @@ ssl=yes # use ssl-support. Works with
# password=ddns_password
# redirect=2
# example.com
##
## Email Only
##
# protocol=emailonly
# host.example.com
##
## dnsHome.de
##
# protocol=dyndns2 \
# server=www.dnshome.de \
# login=subdomain.domain.tld \
# password=your_password \
# subdomain.domain.tld

File diff suppressed because it is too large Load diff

203
t/dnsexit2.pl Normal file
View file

@ -0,0 +1,203 @@
use Test::More;
eval { require JSON::PP; } or plan(skip_all => $@);
JSON::PP->import(qw(encode_json decode_json));
eval { require 'ddclient'; } or BAIL_OUT($@);
eval { require ddclient::Test::Fake::HTTPD; } or plan(skip_all => $@);
eval { require LWP::UserAgent; } or plan(skip_all => $@);
ddclient::load_json_support('dnsexit2');
my @requests; # Declare global variable to store requests, used for tests.
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) = @_;
if ($req->uri->as_string eq '/get_requests') {
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({
code => 0,
message => 'Success'
})]];
});
diag(sprintf("started IPv4 server running at %s", $httpd->endpoint()));
my $ua = LWP::UserAgent->new;
sub test_nic_dnsexit2_update {
my ($config, @hostnames) = @_;
%ddclient::config = %$config;
ddclient::nic_dnsexit2_update(@hostnames);
}
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 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 {
my %config = (
'host.my.zone.com' => {
'ssl' => 'no',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888',
'protocol' => 'dnsexit2',
'password' => 'mytestingpassword',
'zone' => 'my.zone.com',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 5
});
test_nic_dnsexit2_update(\%config, 'host.my.zone.com');
@requests = get_requests();
is($requests[0]->{method}, 'POST', 'Method is correct');
is($requests[0]->{uri}, '/update', 'URI contains correct path');
like($requests[0]->{headers}, qr/Content-Type: application\/json/, 'Content-Type header is correct');
like($requests[0]->{headers}, qr/Accept: application\/json/, 'Accept header is correct');
my $data = decode_and_sort_array($requests[0]->{content});
my $expected_data = decode_and_sort_array({
'domain' => 'my.zone.com',
'apikey' => 'mytestingpassword',
'update' => [
{
'type' => 'A',
'name' => 'host',
'content' => '8.8.4.4',
'ttl' => 5,
},
{
'type' => 'AAAA',
'name' => 'host',
'content' => '2001:4860:4860::8888',
'ttl' => 5,
}
]
});
is_deeply($data, $expected_data, 'Data is correct');
reset_test_data();
};
subtest 'Testing nic_dnsexit2_update without a zone set' => sub {
my %config = (
'myhost.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'anotherpassword',
'server' => $httpd->host_port(),
'path' => '/update-alt',
'ttl' => 10
});
test_nic_dnsexit2_update(\%config, 'myhost.zone.com');
@requests = get_requests();
my $data = decode_and_sort_array($requests[0]->{content});
my $expected_data = decode_and_sort_array({
'domain' => 'myhost.zone.com',
'apikey' => 'anotherpassword',
'update' => [
{
'type' => 'A',
'name' => '',
'content' => '8.8.4.4',
'ttl' => 10,
}
]
});
is_deeply($data, $expected_data, 'Data is correct');
reset_test_data($ua);
};
subtest 'Testing nic_dnsexit2_update with two hostnames, one with a zone and one without' => sub {
my %config = (
'host1.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev4' => 'ipv4',
'wantipv4' => '8.8.4.4',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 5
},
'host2.zone.com' => {
'ssl' => 'yes',
'verbose' => 'yes',
'usev6' => 'ipv6',
'wantipv6' => '2001:4860:4860::8888',
'protocol' => 'dnsexit2',
'password' => 'testingpassword',
'server' => $httpd->host_port(),
'path' => '/update',
'ttl' => 10,
'zone' => 'zone.com'
}
);
test_nic_dnsexit2_update(\%config, 'host1.zone.com', 'host2.zone.com');
my $expected_data1 = decode_and_sort_array({
'domain' => 'host1.zone.com',
'apikey' => 'testingpassword',
'update' => [
{
'type' => 'A',
'name' => '',
'content' => '8.8.4.4',
'ttl' => 5,
}
]
});
my $expected_data2 = decode_and_sort_array({
'domain' => 'zone.com',
'apikey' => 'testingpassword',
'update' => [
{
'type' => 'AAAA',
'name' => 'host2',
'content' => '2001:4860:4860::8888',
'ttl' => 10,
}
]
});
@requests = get_requests();
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();

View file

@ -39,23 +39,30 @@ subtest "get_ip_from_interface tests" => sub {
}
};
subtest "Get default interface and IP for test system" => sub {
subtest "Get default interface and IP for test system (IPv4)" => sub {
my $interface = ddclient::get_default_interface(4);
if ($interface) {
plan(skip_all => 'no IPv4 interface') if !$interface;
isnt($interface, "lo", "Check for loopback 'lo'");
isnt($interface, "lo0", "Check for loopback 'lo0'");
my $ip1 = ddclient::get_ip_from_interface("default", 4);
my $ip2 = ddclient::get_ip_from_interface($interface, 4);
is($ip1, $ip2, "Check IPv4 from default interface");
SKIP: {
skip('default interface does not have an appropriate IPv4 addresses') if !$ip1;
ok(ddclient::is_ipv4($ip1), "Valid IPv4 from get_ip_from_interface($interface)");
}
$interface = ddclient::get_default_interface(6);
if ($interface) {
};
subtest "Get default interface and IP for test system (IPv6)" => sub {
my $interface = ddclient::get_default_interface(6);
plan(skip_all => 'no IPv6 interface') if !$interface;
isnt($interface, "lo", "Check for loopback 'lo'");
isnt($interface, "lo0", "Check for loopback 'lo0'");
my $ip1 = ddclient::get_ip_from_interface("default", 6);
my $ip2 = ddclient::get_ip_from_interface($interface, 6);
is($ip1, $ip2, "Check IPv6 from default interface");
SKIP: {
skip('default interface does not have an appropriate IPv6 addresses') if !$ip1;
ok(ddclient::is_ipv6($ip1), "Valid IPv6 from get_ip_from_interface($interface)");
}
};

27
t/geturl_response.pl Normal file
View file

@ -0,0 +1,27 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
# Fake curl. Use the printf utility, which can process escapes. This allows Perl to drive the fake
# curl with plain ASCII and get arbitrary bytes back, avoiding problems caused by any encoding that
# might be done by Perl (e.g., "use open ':encoding(UTF-8)';").
my @fakecurl = ('sh', '-c', 'printf %b "$1"', '--');
my @test_cases = (
{
desc => 'binary body',
# Body is UTF-8 encoded ✨ (U+2728 Sparkles) followed by a 0xff byte (invalid UTF-8).
printf => join('\r\n', ('HTTP/1.1 200 OK', '', '\0342\0234\0250\0377')),
# The raw bytes should come through as equally valued codepoints. They must not be decoded.
want => "HTTP/1.1 200 OK\n\n\xe2\x9c\xa8\xff",
},
);
for my $tc (@test_cases) {
@ddclient::curl = (@fakecurl, $tc->{printf});
$ddclient::curl if 0; # suppress spurious warning "Name used only once: possible typo"
my $got = ddclient::geturl(url => 'http://ignored');
is($got, $tc->{want}, $tc->{desc});
}
done_testing();

110
t/group_hosts_by.pl Normal file
View file

@ -0,0 +1,110 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
eval { require Data::Dumper; } or skip($@, 1);
Data::Dumper->import();
my $h1 = 'h1';
my $h2 = 'h2';
my $h3 = 'h3';
$ddclient::config{$h1} = {
common => 'common',
h1h2 => 'h1 and h2',
unique => 'h1',
falsy => 0,
maybeunset => 'unique',
};
$ddclient::config{$h2} = {
common => 'common',
h1h2 => 'h1 and h2',
unique => 'h2',
falsy => '',
maybeunset => undef, # should not be grouped with unset
};
$ddclient::config{$h3} = {
common => 'common',
h1h2 => 'unique',
unique => 'h3',
falsy => undef,
# maybeunset is intentionally not set
};
my @test_cases = (
{
desc => 'empty attribute set yields single group with all hosts',
groupby => [qw()],
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
},
{
desc => 'common attribute yields single group with all hosts',
groupby => [qw(common)],
want => [{cfg => {common => 'common'}, hosts => [$h1, $h2, $h3]}],
},
{
desc => 'subset share a value',
groupby => [qw(h1h2)],
want => [
{cfg => {h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
{cfg => {h1h2 => 'unique'}, hosts => [$h3]},
],
},
{
desc => 'all unique',
groupby => [qw(unique)],
want => [
{cfg => {unique => 'h1'}, hosts => [$h1]},
{cfg => {unique => 'h2'}, hosts => [$h2]},
{cfg => {unique => 'h3'}, hosts => [$h3]},
],
},
{
desc => 'combination',
groupby => [qw(common h1h2)],
want => [
{cfg => {common => 'common', h1h2 => 'h1 and h2'}, hosts => [$h1, $h2]},
{cfg => {common => 'common', h1h2 => 'unique'}, hosts => [$h3]},
],
},
{
desc => 'falsy values',
groupby => [qw(falsy)],
want => [
{cfg => {falsy => 0}, hosts => [$h1]},
{cfg => {falsy => ''}, hosts => [$h2]},
{cfg => {falsy => undef}, hosts => [$h3]},
],
},
{
desc => 'set, unset, undef',
groupby => [qw(maybeunset)],
want => [
{cfg => {maybeunset => 'unique'}, hosts => [$h1]},
{cfg => {maybeunset => undef}, hosts => [$h2]},
{cfg => {}, hosts => [$h3]},
],
},
{
desc => 'missing attribute',
groupby => [qw(thisdoesnotexist)],
want => [{cfg => {}, hosts => [$h1, $h2, $h3]}],
},
);
for my $tc (@test_cases) {
my @got = ddclient::group_hosts_by([$h1, $h2, $h3], @{$tc->{groupby}});
# @got is used as a set of sets. Sort everything to make comparison easier.
$_->{hosts} = [sort(@{$_->{hosts}})] for @got;
@got = sort({
for (my $i = 0; $i < @{$a->{hosts}} && $i < @{$b->{hosts}}; ++$i) {
my $x = $a->{hosts}[$i] cmp $b->{hosts}[$i];
return $x if $x != 0;
}
return @{$a->{hosts}} <=> @{$b->{hosts}};
} @got);
is_deeply(\@got, $tc->{want}, $tc->{desc})
or diag(Data::Dumper->new([\@got, $tc->{want}],
[qw(got want)])->Sortkeys(1)->Useqq(1)->Dump());
}
done_testing();

74
t/header_ok.pl Normal file
View file

@ -0,0 +1,74 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my $have_mock = eval { require Test::MockModule; };
my $failmsg;
my $module;
if ($have_mock) {
$module = Test::MockModule->new('ddclient');
# Note: 'mock' is used instead of 'redefine' because 'redefine' is not available in the versions
# of Test::MockModule distributed with old Debian and Ubuntu releases.
$module->mock('failed', sub { $failmsg //= ''; $failmsg .= sprintf(shift, @_) . "\n"; });
}
my @test_cases = (
{
desc => 'malformed not OK',
input => 'malformed',
want => 0,
wantmsg => qr/unexpected/,
},
{
desc => 'HTTP/1.1 200 OK',
input => 'HTTP/1.1 200 OK',
want => 1,
},
{
desc => 'HTTP/2 200 OK',
input => 'HTTP/2 200 OK',
want => 1,
},
{
desc => 'HTTP/3 200 OK',
input => 'HTTP/3 200 OK',
want => 1,
},
{
desc => '401 not OK, fallback message',
input => 'HTTP/1.1 401 ',
want => 0,
wantmsg => qr/authentication failed/,
},
{
desc => '403 not OK, fallback message',
input => 'HTTP/1.1 403 ',
want => 0,
wantmsg => qr/not authorized/,
},
{
desc => 'other 4xx not OK',
input => 'HTTP/1.1 456 bad',
want => 0,
wantmsg => qr/bad/,
},
{
desc => 'only first line is logged on error',
input => "HTTP/1.1 404 not found\n\nbody",
want => 0,
wantmsg => qr/(?!body)/,
},
);
for my $tc (@test_cases) {
subtest $tc->{desc} => sub {
$failmsg = '';
is(ddclient::header_ok('host', $tc->{input}), $tc->{want}, 'return value matches');
SKIP: {
skip('Test::MockModule not available') if !$have_mock;
like($failmsg, $tc->{wantmsg} // qr/^$/, 'fail message matches');
}
};
}
done_testing();

51
t/interval_expired.pl Normal file
View file

@ -0,0 +1,51 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my $h = 't/interval_expired.pl';
my $default_now = 1000000000;
my @test_cases = (
{
interval => 'inf',
want => 0,
},
{
now => 'inf',
interval => 'inf',
want => 0,
},
{
cache => '-inf',
interval => 'inf',
want => 0,
},
{
cache => undef, # Falsy cache value.
interval => 'inf',
want => 0,
},
{
now => 0,
cache => 0, # Different kind of falsy cache value.
interval => 'inf',
want => 0,
},
);
for my $tc (@test_cases) {
$tc->{now} //= $default_now;
# For convenience, $tc->{cache} is an offset from $tc->{now}, not an absolute time..
my $cachetime = $tc->{now} + $tc->{cache} if defined($tc->{cache});
$ddclient::config{$h} = {'interval' => $tc->{interval}};
%ddclient::config if 0; # suppress spurious warning "Name used only once: possible typo"
$ddclient::cache{$h} = {'cached-time' => $cachetime} if defined($cachetime);
%ddclient::cache if 0; # suppress spurious warning "Name used only once: possible typo"
$ddclient::now = $tc->{now};
$ddclient::now if 0; # suppress spurious warning "Name used only once: possible typo"
my $desc = "now=$tc->{now}, cache=${\($cachetime // 'undef')}, interval=$tc->{interval}";
is(ddclient::interval_expired($h, 'cached-time', 'interval'), $tc->{want}, $desc);
}
done_testing();

32
t/variable_defaults.pl Normal file
View file

@ -0,0 +1,32 @@
use Test::More;
SKIP: { eval { require Test::Warnings; } or skip($@, 1); }
eval { require 'ddclient'; } or BAIL_OUT($@);
my %variable_collections = (
map({ ($_ => $ddclient::variables{$_}) } grep($_ ne 'merged', keys(%ddclient::variables))),
map({ ("protocol=$_" => $ddclient::protocols{$_}{variables}); } keys(%ddclient::protocols)),
);
my %seen;
my @test_cases = (
map({
my $vcn = $_;
my $vc = $variable_collections{$_};
map({
my $def = $vc->{$_};
my $seen = exists($seen{$def});
$seen{$def} = undef;
({desc => "$vcn $_", def => $vc->{$_}}) x !$seen;
} sort(keys(%$vc)));
} sort(keys(%variable_collections))),
);
for my $tc (@test_cases) {
if ($tc->{def}{required}) {
is($tc->{def}{default}, undef, "'$tc->{desc}' (required) has no default");
} else {
my $norm;
my $valid = eval { $norm = ddclient::check_value($tc->{def}{default}, $tc->{def}); 1; };
ok($valid, "'$tc->{desc}' (optional) has a valid default");
is($norm, $tc->{def}{default}, "'$tc->{desc}' default normalizes to itself") if $valid;
}
}
done_testing();

View file

@ -35,7 +35,7 @@ my @test_cases = (
for my $tc (@test_cases) {
$warning = undef;
ddclient::write_cache($tc->{f});
ddclient::write_recap($tc->{f});
subtest $tc->{name} => sub {
if (defined($tc->{warning_regex})) {
like($warning, $tc->{warning_regex}, "expected warning message");