From: Martin Basti Date: Wed, 22 Jul 2026 14:08:43 +0000 (+0000) Subject: Migrate digdelv dig formatting checks to pytest X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=8d169d554e658ef4870d8202083511fcaaae78ac;p=thirdparty%2Fbind9.git Migrate digdelv dig formatting checks to pytest Move the dig output-formatting and command-line handling checks from tests.sh into a new tests_dig.py module: +short, +split/+sp, +unknownformat, reverse lookups, the rrcomments/multi matrix, +header-only, +ttlunits, +expandaaaa, +bufsize, query-time units and +yaml timestamp precision, +nocrypto, class CHAOS, query-name error handling and the UPDATE-opcode rejection. Repeated checks are parametrized and the duplicated "+short +rrcomments" check is folded into a single test. The shared check_ttl_range helper mirrors the original awk check: it scans the output line by line, accepts the class tokens IN and CLASS1 (the RFC 3597 unknown-format spelling), and compares the RR type token textually and the TTL numerically. Assisted-by: Claude:claude-fable-5 --- diff --git a/bin/tests/system/digdelv/common.py b/bin/tests/system/digdelv/common.py index dc1bb200f8d..696cd2674be 100644 --- a/bin/tests/system/digdelv/common.py +++ b/bin/tests/system/digdelv/common.py @@ -25,3 +25,22 @@ ARTIFACTS = [ "ns2/example.db", "ns2/example.tld.db", ] + + +def check_ttl_range(text, rrtype, max_ttl): + """Check that a record of the given RR type and class IN (or its + unknown-format spelling CLASS1) is present with a TTL not exceeding + max_ttl.""" + for line in text.splitlines(): + fields = line.split() + if len(fields) < 4: + continue + if fields[2] not in ("IN", "CLASS1") or fields[3] != rrtype: + continue + try: + ttl = int(fields[1]) + except ValueError: + continue + if ttl <= max_ttl: + return True + return False diff --git a/bin/tests/system/digdelv/conftest.py b/bin/tests/system/digdelv/conftest.py new file mode 100644 index 00000000000..530000f2ed5 --- /dev/null +++ b/bin/tests/system/digdelv/conftest.py @@ -0,0 +1,41 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +""" +Fixtures shared by the digdelv test modules. +""" + +from dataclasses import dataclass + +import os + +import pytest + + +@dataclass +class Zsk: + keyid: str + keydata: str + rrcomment: str + + +@pytest.fixture(name="zsk") +def zsk_fixture(): + """Key id and rdata of the ZSK generated for the example zone.""" + with open("ns2/keyid", encoding="utf-8") as keyid_file: + keyid = keyid_file.read().strip() + with open("ns2/keydata", encoding="utf-8") as keydata_file: + keydata = keydata_file.read().strip() + return Zsk( + keyid=keyid, + keydata=keydata, + rrcomment=f"; ZSK; alg = {os.environ['DEFAULT_ALGORITHM']} ; key id = {keyid}", + ) diff --git a/bin/tests/system/digdelv/tests.sh b/bin/tests/system/digdelv/tests.sh index 1d3c0c85145..da3cd6b106e 100644 --- a/bin/tests/system/digdelv/tests.sh +++ b/bin/tests/system/digdelv/tests.sh @@ -74,172 +74,6 @@ HAS_PYYAML=0 $PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1 if [ -x "$DIG" ]; then - n=$((n + 1)) - echo_i "check dig handles UPDATE response ($n)" - ret=0 - dig_with_opts @10.53.0.6 +tries=1 +timeout=1 cname foo.bar >dig.out.test$n 2>&1 && ret=1 - grep "Opcode mismatch" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig short form works ($n)" - ret=0 - dig_with_opts @10.53.0.3 +short a a.example >dig.out.test$n || ret=1 - test "$(wc -l dig.out.test$n || ret=1 - grep " 9ABC DEF6 7890 " /dev/null || ret=1 - check_ttl_range dig.out.test$n "SSHFP" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +unknownformat works ($n)" - ret=0 - dig_with_opts @10.53.0.3 +unknownformat a a.example >dig.out.test$n || ret=1 - grep "CLASS1[ ][ ]*TYPE1[ ][ ]*\\\\# 4 0A000001" /dev/null || ret=1 - check_ttl_range dig.out.test$n "TYPE1" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig with reverse lookup works ($n)" - ret=0 - dig_with_opts @10.53.0.3 -x 127.0.0.1 >dig.out.test$n 2>&1 || ret=1 - # doesn't matter if has answer - grep -i "127\\.in-addr\\.arpa\\." /dev/null || ret=1 - check_ttl_range dig.out.test$n "SOA" 86400 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig -x rejects deeply nested input cleanly ($n)" - ret=0 - longinput="$(printf '1.%.0s' $(seq 1 6400))1" - # Pre-fix: SIGSEGV (139) or ASan abort (134) from unbounded recursion in - # reverse_octets() on the dots. Post-fix: structured rejection via - # DNS_R_NAMETOOLONG -> "Invalid IP address" -> exit 1. - rc=0 - dig_with_opts -x "$longinput" >dig.out.test$n 2>&1 || rc=$? - [ $rc -ge 128 ] && ret=1 - grep "Invalid IP address" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig over TCP works ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 a a.example >dig.out.test$n || ret=1 - grep "10\\.0\\.0\\.1$" /dev/null || ret=1 - check_ttl_range dig.out.test$n "A" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +multi +norrcomments works for DNSKEY (when default is rrcomments)($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +multi +norrcomments -t DNSKEY example >dig.out.test$n || ret=1 - grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" dig.out.test$n >/dev/null && ret=1 - check_ttl_range dig.out.test$n "DNSKEY" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +multi +norrcomments works for SOA (when default is rrcomments)($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +multi +norrcomments -t SOA example >dig.out.test$n || ret=1 - grep "; serial" dig.out.test$n >/dev/null && ret=1 - check_ttl_range dig.out.test$n "SOA" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +rrcomments works for DNSKEY($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +rrcomments DNSKEY example >dig.out.test$n || ret=1 - grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" /dev/null || ret=1 - check_ttl_range dig.out.test$n "DNSKEY" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +short +rrcomments works for DNSKEY ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1 - grep "; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID" /dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +short +nosplit works($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +short +nosplit DNSKEY example >dig.out.test$n || ret=1 - grep "$NOSPLIT" /dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +short +rrcomments works($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1 - grep -q "$KEYDATA ; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID\$" dig.out.nn.$n || ret=1 - dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +multi example +nomulti >dig.out.mn.$n || ret=1 - dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +nomulti example +multi >dig.out.nm.$n || ret=1 - dig_with_opts +tcp @10.53.0.3 -t DNSKEY example +multi example +multi >dig.out.mm.$n || ret=1 - lcnn=$(wc -l dig.out.test$n || ret=1 - grep "Got answer:" /dev/null || ret=1 - check_ttl_range dig.out.test$n "SOA" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig +short +rrcomments works($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +short +rrcomments DNSKEY example >dig.out.test$n || ret=1 - grep -q "$KEYDATA ; ZSK; alg = $DEFAULT_ALGORITHM ; key id = $KEYID\$" dig.out.test$n || ret=1 - grep "^;; flags: qr rd; QUERY: 0, ANSWER: 0," /dev/null || ret=1 - grep "^;; QUESTION SECTION:" /dev/null && ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "checking dig +coflag works ($n)" ret=0 @@ -281,15 +115,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "checking dig +header-only works (with class and type set) ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.3 +header-only -c IN -t A example >dig.out.test$n || ret=1 - grep "^;; flags: qr rd; QUERY: 0, ANSWER: 0," /dev/null || ret=1 - grep "^;; QUESTION SECTION:" /dev/null && ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "checking dig +zflag works, and that BIND properly ignores it ($n)" ret=0 @@ -309,34 +134,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "checking dig +ttlunits works ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +ttlunits A weeks.example >dig.out.test$n || ret=1 - grep "^weeks.example. 3w" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +ttlunits A days.example >dig.out.test$n || ret=1 - grep "^days.example. 3d" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +ttlunits A hours.example >dig.out.test$n || ret=1 - grep "^hours.example. 3h" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +ttlunits A minutes.example >dig.out.test$n || ret=1 - grep "^minutes.example. 45m" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +ttlunits A seconds.example >dig.out.test$n || ret=1 - grep "^seconds.example. 45s" /dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig respects precedence of options with +ttlunits ($n)" - ret=0 - dig_with_opts +tcp @10.53.0.2 +ttlunits +nottlid A weeks.example >dig.out.test$n || ret=1 - grep "^weeks.example. IN" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +nottlid +ttlunits A weeks.example >dig.out.test$n || ret=1 - grep "^weeks.example. 3w" /dev/null || ret=1 - dig_with_opts +tcp @10.53.0.2 +nottlid +nottlunits A weeks.example >dig.out.test$n || ret=1 - grep "^weeks.example. 1814400" /dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "checking dig preserves origin on TCP retries ($n)" ret=0 @@ -533,23 +330,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "checking dig +sp works as an abbreviated form of split ($n)" - ret=0 - dig_with_opts @10.53.0.3 +sp=4 -t sshfp foo.example >dig.out.test$n || ret=1 - grep " 9ABC DEF6 7890 " /dev/null || ret=1 - check_ttl_range dig.out.test$n "SSHFP" 300 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking dig -c works ($n)" - ret=0 - dig_with_opts @10.53.0.3 -c CHAOS -t txt version.bind >dig.out.test$n || ret=1 - grep "version.bind. 0 CH TXT" /dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "checking dig +ednsopt with option number ($n)" ret=0 @@ -649,14 +429,6 @@ if [ -x "$DIG" ]; then status=$((status + ret)) fi - n=$((n + 1)) - echo_i "checking that dig warns about .local queries ($n)" - ret=0 - dig_with_opts @10.53.0.3 local soa >dig.out.test$n 2>&1 || ret=1 - grep ";; WARNING: .local is reserved for Multicast DNS" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "check that dig processes +ednsopt=key-tag and FORMERR is returned ($n)" ret=0 @@ -1038,28 +810,6 @@ if [ -x "$DIG" ]; then status=$((status + ret)) fi - n=$((n + 1)) - echo_i "check that dig gracefully handles bad escape in domain name ($n)" - ret=0 - digstatus=0 - dig_with_opts @10.53.0.3 '\0.' >dig.out.test$n 2>&1 || digstatus=$? - echo digstatus=$digstatus >>dig.out.test$n - test $digstatus -eq 10 || ret=1 - grep REQUIRE dig.out.test$n >/dev/null && ret=1 - grep "is not a legal name (bad escape)" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig -q -m works ($n)" - ret=0 - dig_with_opts @10.53.0.3 -q -m >dig.out.test$n 2>&1 - pat='^;-m\..*IN.*A$' - grep "$pat" dig.out.test$n >/dev/null || ret=1 - grep "Dump of all outstanding memory allocations" dig.out.test$n >/dev/null && ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - n=$((n + 1)) echo_i "checking exit code for a retry upon TCP EOF (immediate -> immediate) ($n)" ret=0 @@ -1139,40 +889,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "check that dig +expandaaaa works ($n)" - ret=0 - dig_with_opts @10.53.0.3 +expandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1 - grep "ns2.example.*fd92:7065:0b8e:ffff:0000:0000:0000:0002" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig +noexpandaaaa works ($n)" - ret=0 - dig_with_opts @10.53.0.3 +noexpandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1 - grep "ns2.example.*fd92:7065:b8e:ffff::2" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig default for +[no]expandaaa (+noexpandaaaa) works ($n)" - ret=0 - dig_with_opts @10.53.0.3 AAAA ns2.example >dig.out.test$n 2>&1 || ret=1 - grep "ns2.example.*fd92:7065:b8e:ffff::2" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - - echo_i "check that dig +short +expandaaaa works ($n)" - ret=0 - dig_with_opts @10.53.0.3 +short +expandaaaa AAAA ns2.example >dig.out.test$n 2>&1 || ret=1 - pat='^fd92:7065:0b8e:ffff:0000:0000:0000:0002$' - grep "$pat" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - if [ $HAS_PYYAML -ne 0 ]; then n=$((n + 1)) echo_i "check dig +yaml ANY output ($n)" @@ -1242,59 +958,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "check that dig +bufsize=0 just sets the buffer size to 0 ($n)" - ret=0 - dig_with_opts @10.53.0.3 a.example +bufsize=0 +qr >dig.out.test$n 2>&1 || ret=1 - grep "EDNS:" dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig +bufsize restores default bufsize ($n)" - ret=0 - dig_with_opts @10.53.0.3 a.example +bufsize=0 +bufsize +qr >dig.out.test$n 2>&1 || ret=1 - lines=$(grep "EDNS:.* udp:" dig.out.test$n | wc -l) - lines1232=$(grep "EDNS:.* udp: 1232" dig.out.test$n | wc -l) - test $lines -eq 2 || ret=1 - test $lines1232 -eq 2 || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig without -u displays 'Query time' in millseconds ($n)" - ret=0 - dig_with_opts @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 - grep ';; Query time: [0-9][0-9]* msec' dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig -u displays 'Query time' in microseconds ($n)" - ret=0 - dig_with_opts -u @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 - grep ';; Query time: [0-9][0-9]* usec' dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig +yaml without -u displays timestamps in milliseconds ($n)" - ret=0 - dig_with_opts +yaml @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 - grep 'query_time: !!timestamp ....-..-..T..:..:..\....Z' dig.out.test$n >/dev/null || ret=1 - grep 'response_time: !!timestamp ....-..-..T..:..:..\....Z' dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "check that dig -u +yaml displays timestamps in microseconds ($n)" - ret=0 - dig_with_opts -u +yaml @10.53.0.3 a.example >dig.out.test$n 2>&1 || ret=1 - grep 'query_time: !!timestamp ....-..-..T..:..:..\.......Z' dig.out.test$n >/dev/null || ret=1 - grep 'response_time: !!timestamp ....-..-..T..:..:..\.......Z' dig.out.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - # See [GL #3020] for more information n=$((n + 1)) echo_i "check that dig handles UDP timeout followed by a SERVFAIL correctly ($n)" @@ -1444,18 +1107,6 @@ if [ -x "$DIG" ]; then if [ $ret -ne 0 ]; then echo_i "failed"; fi status=$((status + ret)) - n=$((n + 1)) - echo_i "check dig's +nocrypto flag ($n)" - ret=0 - dig_with_opts +dnssec +norec +nocrypto DNSKEY . @10.53.0.1 >dig.out.dnskey.test$n || ret=1 - grep -E "256 [0-9]+ $DEFAULT_ALGORITHM_NUMBER \\[key id = [1-9][0-9]*]" dig.out.dnskey.test$n >/dev/null || ret=1 - grep -E "RRSIG.* \\[omitted]" dig.out.dnskey.test$n >/dev/null || ret=1 - dig_with_opts +norec +nocrypto DS example \ - @10.53.0.1 >dig.out.ds.test$n || ret=1 - grep -E "DS.* [0-9]+ [12] \[omitted]" dig.out.ds.test$n >/dev/null || ret=1 - if [ $ret -ne 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - # See GL#5609 n=$((n + 1)) echo_i "check dig with a IPv4 source address and a server with both IPv4 and IPv6 addresses doesn't crash ($n)" diff --git a/bin/tests/system/digdelv/tests_dig.py b/bin/tests/system/digdelv/tests_dig.py new file mode 100644 index 00000000000..6b89877cc82 --- /dev/null +++ b/bin/tests/system/digdelv/tests_dig.py @@ -0,0 +1,311 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +""" +Tests for the dig tool. +""" + +from re import compile as Re + +import os +import re + +import pytest + +from digdelv.common import ARTIFACTS, check_ttl_range +from isctest.util import param + +import isctest + +pytestmark = [ + pytest.mark.skipif( + not os.access(os.environ.get("DIG", ""), os.X_OK), + reason="dig executable not available", + ), + pytest.mark.extra_artifacts(ARTIFACTS), +] + + +@pytest.fixture(name="dig") +def dig_fixture(named_port): + return isctest.run.EnvCmd("DIG", f"-p {named_port}") + + +def test_update_response(dig, ans6): + """Check that dig rejects a response with the UPDATE opcode.""" + result = dig( + f"@{ans6.ip} +tries=1 +timeout=1 cname foo.bar", raise_on_exception=False + ) + assert result.rc != 0 + assert "Opcode mismatch" in result.out + + +def test_short(dig, ns3): + """Check that dig +short returns a single-line answer.""" + result = dig(f"@{ns3.ip} +short a a.example") + assert len(result.out.splitlines()) == 1 + + +@pytest.mark.parametrize("option", ["+split=4", "+sp=4"]) +def test_split_width(dig, ns3, option): + """Check that dig +split (and its +sp abbreviation) splits hex data + into fields of the requested width.""" + result = dig(f"@{ns3.ip} {option} -t sshfp foo.example") + assert " 9ABC DEF6 7890 " in result.out + assert check_ttl_range(result.out, "SSHFP", 300) + + +def test_unknownformat(dig, ns3): + """Check that dig +unknownformat prints RFC 3597 format.""" + result = dig(f"@{ns3.ip} +unknownformat a a.example") + assert Re(r"CLASS1\s+TYPE1\s+\\# 4 0A000001") in result.out + assert check_ttl_range(result.out, "TYPE1", 300) + + +def test_reverse_lookup(dig, ns3): + """Check that dig -x works.""" + result = dig(f"@{ns3.ip} -x 127.0.0.1") + # doesn't matter if has answer + assert Re(r"127\.in-addr\.arpa\.", re.IGNORECASE) in result.out + assert check_ttl_range(result.out, "SOA", 86400) + + +def test_reverse_lookup_deeply_nested(dig): + """Check that dig -x rejects deeply nested input cleanly instead of + crashing on unbounded recursion in reverse_octets().""" + longinput = "1." * 6400 + "1" + result = dig(f"-x {longinput}", raise_on_exception=False) + assert result.rc == 1 + assert "Invalid IP address" in result.err + + +def test_tcp(dig, ns3): + """Check that dig over TCP works.""" + result = dig(f"+tcp @{ns3.ip} a a.example") + assert Re(r"10\.0\.0\.1$") in result.out + assert check_ttl_range(result.out, "A", 300) + + +@pytest.mark.parametrize( + "args,expect_rrcomment", + [ + param("+multi +norrcomments -t DNSKEY example", False, id="multi-norrcomments"), + param("+rrcomments DNSKEY example", True, id="rrcomments"), + param("+short +rrcomments DNSKEY example", True, id="short-rrcomments"), + ], +) +def test_dnskey_rrcomments(dig, ns3, zsk, args, expect_rrcomment): + """Check that +[no]rrcomments controls the DNSKEY comment + (the default is rrcomments, even with +multi).""" + result = dig(f"+tcp @{ns3.ip} {args}") + assert (zsk.rrcomment in result.out) == expect_rrcomment + if "+short" not in args: + assert check_ttl_range(result.out, "DNSKEY", 300) + + +def test_soa_norrcomments(dig, ns3): + """Check that +multi +norrcomments suppresses the SOA field comments.""" + result = dig(f"+tcp @{ns3.ip} +multi +norrcomments -t SOA example") + assert "; serial" not in result.out + assert check_ttl_range(result.out, "SOA", 300) + + +def test_short_nosplit(dig, ns3, zsk): + """Check that dig +short +nosplit does not split the key data.""" + result = dig(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example") + assert zsk.keydata.replace(" ", "") in result.out + + +def test_short_rrcomments_line(dig, ns3, zsk): + """Check the exact dig +short +rrcomments output line.""" + result = dig(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example") + expected = re.escape(f"{zsk.keydata} {zsk.rrcomment}") + assert Re(expected + "$") in result.out + + +def test_multi_flag_is_local(dig, ns3): + """Check that +[no]multi applies to a single lookup only.""" + lines = {} + for flags in [ + ("nomulti", "nomulti"), + ("multi", "nomulti"), + ("nomulti", "multi"), + ("multi", "multi"), + ]: + first, second = flags + result = dig(f"+tcp @{ns3.ip} -t DNSKEY example +{first} example +{second}") + assert check_ttl_range(result.out, "DNSKEY", 300) + lines[flags] = len(result.out.splitlines()) + assert lines[("multi", "multi")] >= lines[("nomulti", "multi")] + assert lines[("multi", "multi")] >= lines[("multi", "nomulti")] + assert lines[("nomulti", "multi")] >= lines[("nomulti", "nomulti")] + assert lines[("multi", "nomulti")] >= lines[("nomulti", "nomulti")] + + +def test_noheader_only(dig, ns3): + """Check that dig +noheader-only sends a full query.""" + result = dig(f"+tcp @{ns3.ip} +noheader-only A example") + assert "Got answer:" in result.out + assert check_ttl_range(result.out, "SOA", 300) + + +@pytest.mark.parametrize( + "class_type", + [ + param("", id="default"), + param("-c IN -t A", id="with-class-and-type"), + ], +) +def test_header_only(dig, ns3, class_type): + """Check that dig +header-only sends a query without a question.""" + result = dig(f"+tcp @{ns3.ip} +header-only {class_type} example") + assert Re(r"^;; flags: qr rd; QUERY: 0, ANSWER: 0,") in result.out + assert Re(r"^;; QUESTION SECTION:") not in result.out + + +@pytest.mark.parametrize( + "qname,ttl", + [ + param("weeks", "3w"), + param("days", "3d"), + param("hours", "3h"), + param("minutes", "45m"), + param("seconds", "45s"), + ], +) +def test_ttl_units(dig, ns2, qname, ttl): + """Check that dig +ttlunits prints TTLs in time units.""" + result = dig(f"+tcp @{ns2.ip} +ttlunits A {qname}.example") + assert Re(rf"^{qname}\.example\.\s+{ttl}\s") in result.out + + +@pytest.mark.parametrize( + "options,field", + [ + param("+ttlunits +nottlid", "IN", id="nottlid-wins"), + param("+nottlid +ttlunits", "3w", id="ttlunits-wins"), + param("+nottlid +nottlunits", "1814400", id="plain-seconds"), + ], +) +def test_ttl_units_precedence(dig, ns2, options, field): + """Check that the last of the +ttlid/+ttlunits options wins.""" + result = dig(f"+tcp @{ns2.ip} {options} A weeks.example") + assert Re(rf"^weeks\.example\.\s+{re.escape(field)}\s") in result.out + + +def test_class_chaos(dig, ns3): + """Check that dig -c CHAOS works.""" + result = dig(f"@{ns3.ip} -c CHAOS -t txt version.bind") + assert "version.bind.\t\t0\tCH\tTXT" in result.out + + +def test_bad_escape(dig, ns3): + """Check that dig gracefully rejects a bad escape in the domain name.""" + result = dig(rf"@{ns3.ip} \0.", raise_on_exception=False) + assert result.rc == 10 + assert "REQUIRE" not in result.err + assert "is not a legal name (bad escape)" in result.err + + +def test_q_m(dig, ns3): + """Check that -q -m treats -m as a query name, not as the memory + debugging flag.""" + result = dig(f"@{ns3.ip} -q -m", raise_on_exception=False) + assert Re(r"^;-m\..*IN.*A$") in result.out + assert "Dump of all outstanding memory allocations" not in result.out + + +@pytest.mark.parametrize( + "options,pattern", + [ + param( + "+expandaaaa", + r"ns2\.example.*fd92:7065:0b8e:ffff:0000:0000:0000:0002", + id="expandaaaa", + ), + param( + "+noexpandaaaa", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="noexpandaaaa" + ), + param("", r"ns2\.example.*fd92:7065:b8e:ffff::2", id="default"), + param( + "+short +expandaaaa", + r"^fd92:7065:0b8e:ffff:0000:0000:0000:0002$", + id="short-expandaaaa", + ), + ], +) +def test_expandaaaa(dig, ns3, options, pattern): + """Check that +[no]expandaaaa controls AAAA address formatting + (the default is +noexpandaaaa).""" + result = dig(f"@{ns3.ip} {options} AAAA ns2.example") + assert Re(pattern) in result.out + + +def test_bufsize_zero(dig, ns3): + """Check that +bufsize=0 just sets the advertised buffer size to 0 + instead of disabling EDNS.""" + result = dig(f"@{ns3.ip} a.example +bufsize=0 +qr") + assert "EDNS:" in result.out + + +def test_bufsize_restores_default(dig, ns3): + """Check that a later +bufsize restores the default buffer size.""" + result = dig(f"@{ns3.ip} a.example +bufsize=0 +bufsize +qr") + assert len(result.out.grep(Re(r"EDNS:.* udp:"))) == 2 + assert len(result.out.grep(Re(r"EDNS:.* udp: 1232"))) == 2 + + +@pytest.mark.parametrize( + "options,unit", + [ + param("", "msec", id="msec"), + param("-u", "usec", id="usec"), + ], +) +def test_query_time_units(dig, ns3, options, unit): + """Check that Query time is in milliseconds, or in microseconds + with -u.""" + result = dig(f"{options} @{ns3.ip} a.example") + assert Re(rf";; Query time: \d+ {unit}") in result.out + + +@pytest.mark.parametrize( + "options,digits", + [ + param("+yaml", 3, id="msec"), + param("-u +yaml", 6, id="usec"), + ], +) +def test_yaml_timestamp_precision(dig, ns3, options, digits): + """Check that +yaml timestamps have millisecond precision, or + microsecond precision with -u.""" + result = dig(f"{options} @{ns3.ip} a.example") + for field in ("query_time", "response_time"): + pattern = ( + rf"{field}: !!timestamp \d{{4}}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d{{{digits}}}Z" + ) + assert Re(pattern) in result.out + + +def test_local_reserved_warning(dig, ns3): + """Check that dig warns about .local queries.""" + result = dig(f"@{ns3.ip} local soa") + assert ";; WARNING: .local is reserved for Multicast DNS" in result.out + + +def test_nocrypto(dig, ns1): + """Check that +nocrypto omits the key and signature data.""" + alg_num = os.environ["DEFAULT_ALGORITHM_NUMBER"] + result = dig(f"+dnssec +norec +nocrypto DNSKEY . @{ns1.ip}") + assert Re(rf"256 \d+ {alg_num} \[key id = [1-9]\d*]") in result.out + assert Re(r"RRSIG.* \[omitted]") in result.out + result = dig(f"+norec +nocrypto DS example @{ns1.ip}") + assert Re(r"DS.* \d+ [12] \[omitted]") in result.out