]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Migrate digdelv dig EDNS checks to pytest
authorMartin Basti <mbasti@isc.org>
Wed, 22 Jul 2026 14:52:10 +0000 (14:52 +0000)
committerMartin Basti <mbasti@isc.org>
Wed, 29 Jul 2026 11:54:10 +0000 (13:54 +0200)
Move the dig EDNS checks from tests.sh into tests_dig.py: the header
and EDNS flag options (+coflag/+raflag/+tcflag/+zflag, +ednsflags,
+showbadvers, +showtruncated), the +ednsopt family (NSID, KEY-TAG,
CLIENT-TAG, SERVER-TAG, CHAIN, EDE, UPDATE-LEASE, LLQ and malformed
options), +expire, +keepalive and +zoneversion, together with their
+yaml counterparts.  The yamlget.py helper script is replaced by a
yaml_get() function which walks the parsed YAML directly, so the +yaml
checks now compare typed values instead of strings, and the client-tag,
server-tag and EDE cases are parametrized.

Assisted-by: Claude:claude-fable-5
bin/tests/system/digdelv/common.py
bin/tests/system/digdelv/tests.sh
bin/tests/system/digdelv/tests_dig.py

index 696cd2674be1d53d38925226bbc1abe9f3fda8f7..e98e25258d62482822b06d1bb34ab4847bcd8a6f 100644 (file)
 Helpers shared by the digdelv test modules.
 """
 
+import pytest
+
+try:
+    import yaml  # type: ignore[import-untyped]
+except ImportError:
+    yaml = None  # type: ignore[assignment]
+
 ARTIFACTS = [
     "ans*/ans.run",
     "ns*/anchor.*",
@@ -26,6 +33,13 @@ ARTIFACTS = [
     "ns2/example.tld.db",
 ]
 
+needs_pyyaml = pytest.mark.skipif(yaml is None, reason="PyYAML is not installed")
+
+
+def parse_yaml(text):
+    """Parse the tools' +yaml output."""
+    return yaml.safe_load(text)
+
 
 def check_ttl_range(text, rrtype, max_ttl):
     """Check that a record of the given RR type and class IN (or its
index da3cd6b106ead9c93c3d9a62bd49d1e0a284ec9d..27f059a4f7a8769f3439d5c0f5068f2a46022cf9 100644 (file)
@@ -74,66 +74,6 @@ HAS_PYYAML=0
 $PYTHON -c "import yaml" 2>/dev/null && HAS_PYYAML=1
 
 if [ -x "$DIG" ]; then
-  n=$((n + 1))
-  echo_i "checking dig +coflag works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +coflag +qr example >dig.out.test$n || ret=1
-  grep "^; EDNS: version: 0, flags: co;" <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))
-
-  if [ $HAS_PYYAML -ne 0 ]; then
-    n=$((n + 1))
-    echo_i "checking dig +coflag +yaml works ($n)"
-    ret=0
-    dig_with_opts +yaml +tcp @10.53.0.3 +coflag +qr example >dig.out.test$n || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS flags >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "co" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "checking dig +raflag works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +raflag +qr example >dig.out.test$n || ret=1
-  grep "^;; flags: rd ra ad; QUERY: 1, ANSWER: 0," <dig.out.test$n >/dev/null || ret=1
-  grep "^;; flags: qr rd ra; QUERY: 1, ANSWER: 0," <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 +tcflag works ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +tcflag +qr example >dig.out.test$n || ret=1
-  grep "^;; flags: tc rd ad; QUERY: 1, ANSWER: 0" <dig.out.test$n >/dev/null || ret=1
-  grep "^;; flags: qr rd ra; QUERY: 1, ANSWER: 0," <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 +zflag works, and that BIND properly ignores it ($n)"
-  ret=0
-  dig_with_opts +tcp @10.53.0.3 +zflag +qr A example >dig.out.test$n || ret=1
-  sed -n '/Sending:/,/Got answer:/p' dig.out.test$n | grep "^;; flags: rd ad; MBZ: 0x4;" >/dev/null || ret=1
-  sed -n '/Got answer:/,/AUTHORITY SECTION:/p' dig.out.test$n | grep "^;; flags: qr rd ra; QUERY: 1" >/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 +qr +ednsopt=08 does not cause an INSIST failure ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=08 +qr a a.example >dig.out.test$n || ret=1
-  grep "INSIST" <dig.out.test$n >/dev/null && ret=1
-  grep "FORMERR" <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 preserves origin on TCP retries ($n)"
   ret=0
@@ -331,484 +271,6 @@ if [ -x "$DIG" ]; then
   status=$((status + ret))
 
   n=$((n + 1))
-  echo_i "checking dig +ednsopt with option number ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=3 a.example >dig.out.test$n 2>&1 || ret=1
-  grep 'NSID: .* ("ns3")' dig.out.test$n >/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 +ednsopt with option name ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=nsid a.example >dig.out.test$n 2>&1 || ret=1
-  grep 'NSID: .* ("ns3")' dig.out.test$n >/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 ednsopt UPDATE-LEASE prints as expected (single lease) ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=UPDATE-LEASE:00000e10 +qr a.example >dig.out.test$n 2>&1 || ret=1
-  pat='UPDATE-LEASE: 3600 (1 hour)'
-  grep "$pat" dig.out.test$n >/dev/null || ret=1
-  if [ $ret -ne 0 ]; then echo_i "failed"; fi
-  status=$((status + ret))
-  n=$((n + 1))
-
-  if [ $HAS_PYYAML -ne 0 ]; then
-    n=$((n + 1))
-    echo_i "checking ednsopt UPDATE-LEASE prints as expected (single lease) +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=UPDATE-LEASE:00000e10 +qr a.example >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS UPDATE-LEASE LEASE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "3600" ] || ret=1
-    grep "LEASE: 3600 # 1 hour" dig.out.test$n >/dev/null || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "checking ednsopt UPDATE-LEASE prints as expected (split lease) ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example >dig.out.test$n 2>&1 || ret=1
-  pat='UPDATE-LEASE: 3600/1209600 (1 hour/2 weeks)'
-  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 "checking ednsopt UPDATE-LEASE prints as expected (split lease) +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS UPDATE-LEASE LEASE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "3600" ] || ret=1
-    grep "LEASE: 3600 # 1 hour" dig.out.test$n >/dev/null || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS UPDATE-LEASE KEY-LEASE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1209600" ] || ret=1
-    grep "KEY-LEASE: 1209600 # 2 weeks" dig.out.test$n >/dev/null || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  echo_i "checking ednsopt LLQ prints as expected ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=llq:0001000200001234567812345678fefefefe +qr a.example >dig.out.test$n 2>&1 || ret=1
-  pat='LLQ: Version: 1, Opcode: 2, Error: 0, Identifier: 1311768465173141112, Lifetime: 4278124286$'
-  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 "checking ednsopt LLQ prints as expected +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=llq:0001000200001234567812345678fefefefe +qr a.example >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS LLQ LLQ-VERSION >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS LLQ LLQ-OPCODE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "2" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS LLQ LLQ-ERROR >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "0" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS LLQ LLQ-ID >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1311768465173141112" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS LLQ LLQ-LEASE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "4278124286" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that dig processes +ednsopt=key-tag and FORMERR is returned ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=key-tag a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; KEY-TAG: *$" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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:<value-list> ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=key-tag:00010002 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; KEY-TAG: 1, 2$" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" dig.out.test$n >/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))
-
-  if [ $HAS_PYYAML -ne 0 ]; then
-    n=$((n + 1))
-    ret=0
-    echo_i "check that dig processes +ednsopt=key-tag:<value-list> +yaml ($n)"
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=key-tag:00010002 a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS KEY-TAG >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "[1, 2]" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that dig processes +ednsopt=key-tag:<malformed-value-list> and FORMERR is returned ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=key-tag:0001000201 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; KEY-TAG: 00 01 00 02 01" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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=client-tag:value ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=client-tag:0001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; CLIENT-TAG: 1$" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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 that dig processes +ednsopt=client-tag:value +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=client-tag:0001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS CLIENT-TAG >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that FORMERR is returned for a too short client-tag ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=client-tag:01 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; CLIENT-TAG" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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 FORMERR is returned for a too long client-tag ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=client-tag:000001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; CLIENT-TAG" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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=server-tag:value ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=server-tag:0001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; SERVER-TAG: 1$" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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 that dig processes +ednsopt=server-tag:value +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=server-tag:0001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS SERVER-TAG >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that FORMERR is returned for a too short server-tag ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=server-tag:01 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; SERVER-TAG" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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 FORMERR is returned for a too long server-tag ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=server-tag:000001 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  grep "; SERVER-TAG" dig.out.test$n >/dev/null || ret=1
-  grep "status: FORMERR" 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=chain:02002200 ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=chain:02002200 'a.\000"' +qr >dig.out.test$n 2>&1 || ret=1
-  grep '; CHAIN: "\\000\\""' 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 that dig processes +ednsopt=chain:02002200 +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=chain:02002200 'a.\000"' +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS CHAIN >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = '\000\"' ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that dig processes +expire ($n)"
-  ret=0
-  dig_with_opts @10.53.0.1 +expire . soa >dig.out.test$n 2>&1 || ret=1
-  grep '; EXPIRE: 1200 (20 minutes)' 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 that dig processes +expire +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.1 +yaml +expire . soa >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message response_message_data OPT_PSEUDOSECTION EDNS EXPIRE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "1200" ] || ret=1
-    grep "EXPIRE: 1200 # 20 minutes" dig.out.test$n >/dev/null || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that dig processes +keepalive ($n)"
-  ret=0
-  dig_with_opts @10.53.0.1 +keepalive . soa +tcp >dig.out.test$n 2>&1 || ret=1
-  grep '; TCP-KEEPALIVE: 30.0 secs' 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 that dig processes +keepalive +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.1 +yaml +keepalive . soa +tcp >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message response_message_data OPT_PSEUDOSECTION EDNS TCP-KEEPALIVE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "30.0 secs" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that Extended DNS Error 0 is printed correctly ($n)"
-  ret=0
-  # First defined EDE code, additional text "foo".
-  dig_with_opts @10.53.0.3 +ednsopt=ede:0000666f6f a.example +qr >dig.out.test$n 2>&1 || ret=1
-  pat='^; EDE: 0 (Other): (foo)$'
-  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 that Extended DNS Error 0 is printed correctly +yaml ($n)"
-    ret=0
-    # add specials '"' and '\'
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede:0000666f6f225c a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE EXTRA-TEXT >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = 'foo"\' ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that Extended DNS Error 24 is printed correctly ($n)"
-  ret=0
-  # Last defined EDE code, no additional text.
-  dig_with_opts @10.53.0.3 +ednsopt=ede:0018 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  pat='^; EDE: 24 (Invalid Data)$'
-  grep "$pat" 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 Extended DNS Error 25 is printed correctly ($n)"
-  ret=0
-  # First undefined EDE code, additional text "foo".
-  dig_with_opts @10.53.0.3 +ednsopt=ede:0019666f6f a.example +qr >dig.out.test$n 2>&1 || ret=1
-  pat='^; EDE: 25: (foo)$'
-  grep "$pat" 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 invalid Extended DNS Error (length 0) is printed ($n)"
-  ret=0
-  # EDE payload is too short
-  dig_with_opts @10.53.0.3 +ednsopt=ede a.example +qr >dig.out.test$n 2>&1 || ret=1
-  pat='^; EDE:$'
-  grep "$pat" 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 invalid Extended DNS Error (length 1) is printed ($n)"
-  ret=0
-  # EDE payload is too short
-  dig_with_opts @10.53.0.3 +ednsopt=ede:00 a.example +qr >dig.out.test$n 2>&1 || ret=1
-  pat='^; EDE: 00 (".")$'
-  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 that +yaml Extended DNS Error 0 is printed correctly ($n)"
-    ret=0
-    # First defined EDE code, additional text "foo".
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede:0000666f6f a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE INFO-CODE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "0 (Other)" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE EXTRA-TEXT >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "foo" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check that +yaml Extended DNS Error 24 is printed correctly ($n)"
-    ret=0
-    # Last defined EDE code, no additional text.
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede:0018 a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE INFO-CODE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "24 (Invalid Data)" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE EXTRA-TEXT >yamlget.out.test$n 2>&1 && ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check that +yaml Extended DNS Error 25 is printed correctly ($n)"
-    ret=0
-    # First undefined EDE code, additional text "foo".
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede:0019666f6f a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE INFO-CODE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "25" ] || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE EXTRA-TEXT >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "foo" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check that invalid Extended DNS Error (length 0) is printed ($n)"
-    ret=0
-    # EDE payload is too short
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = "None" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-
-    n=$((n + 1))
-    echo_i "check that invalid +yaml Extended DNS Error (length 1) is printed ($n)"
-    ret=0
-    # EDE payload is too short
-    dig_with_opts @10.53.0.3 +yaml +ednsopt=ede:00 a.example +qr >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS EDE >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    [ "$value" = '00 (".")' ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "check that dig handles malformed option '+ednsopt=:' gracefully ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +ednsopt=: a.example >dig.out.test$n 2>&1 && ret=1
-  grep "ednsopt no code point specified" 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 +zoneversion to a authoritative server ($n)"
-  ret=0
-  dig_with_opts @10.53.0.2 +zoneversion a.example >dig.out.test$n 2>&1 || ret=1
-  pat="; ZONEVERSION: ZONE: example, SOA-SERIAL: 2000042407"
-  grep "$pat" 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 +zoneversion to a authoritative server with zoneversion disabled ($n)"
-  ret=0
-  dig_with_opts @10.53.0.2 +zoneversion a.example.tld >dig.out.test$n 2>&1 || ret=1
-  grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
-  grep "; ZONEVERSION:" 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 "checking dig +yaml +zoneversion to a authoritative server ($n)"
-    ret=0
-    dig_with_opts @10.53.0.2 +yaml +zoneversion a.example >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message response_message_data OPT_PSEUDOSECTION EDNS ZONEVERSION >yamlget.out.test$n 2>&1 || ret=1
-    read -r value <yamlget.out.test$n
-    expected="{'ZONE': 'example', 'SOA-SERIAL': 2000042407}"
-    [ "$value" = "$expected" ] || ret=1
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
-
-  n=$((n + 1))
-  echo_i "checking dig +ednsopt=ZONEVERSION:<answer> to a authoritative server ($n)"
-  ret=0
-  dig_with_opts @10.53.0.2 +ednsopt=ZONEVERSION:0100000007DA a.example >dig.out.test$n 2>&1 || ret=1
-  grep "status: FORMERR" dig.out.test$n >/dev/null || ret=1
-  grep "; ZONEVERSION:" 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 +zoneversion to a recursive server ($n)"
-  ret=0
-  dig_with_opts @10.53.0.3 +zoneversion a.example >dig.out.test$n 2>&1 || ret=1
-  grep '; ZONEVERSION:' 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 display of non serial type zoneversion ($n)"
-  ret=0
-  dig_with_opts @10.53.0.2 +qr +ednsopt=ZONEVERSION:0100000007DA a.example >dig.out.test$n 2>&1 || ret=1
-  grep '; ZONEVERSION: LABELS: 1, TYPE: 0, VALUE: 000007da ("....")' 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 "checking display of non serial type zoneversion +yaml ($n)"
-    ret=0
-    dig_with_opts @10.53.0.2 +qr +ednsopt=ZONEVERSION:0100000007DA a.example +yaml >dig.out.test$n 2>&1 || ret=1
-    $PYTHON yamlget.py dig.out.test$n 0 message query_message_data OPT_PSEUDOSECTION EDNS ZONEVERSION >yamlget.out.test$n 2>&1 || ret=1
-    expected="{'ZONE': 'example', 'TYPE': 1, 'VALUE': 000007da, PVALUE: '....'}"
-    if [ $ret -ne 0 ]; then echo_i "failed"; fi
-    status=$((status + ret))
-  fi
 
   n=$((n + 1))
   echo_i "checking exit code for a retry upon TCP EOF (immediate -> immediate) ($n)"
@@ -1081,32 +543,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 +noedns +ednsflags=<nonzero> re-enables EDNS ($n)"
-  dig_with_opts @10.53.0.3 +qr +noedns +ednsflags=0x70 a.example >dig.out.test$n 2>&1 || ret=1
-  grep "; EDNS: version: 0, flags:; MBZ: 0x0070, udp: 1232" dig.out.test$n >/dev/null || ret=1
-  grep "; EDNS: version: 0, flags:; udp: 1232" 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 +showbadvers works ($n)"
-  dig_with_opts @10.53.0.3 +edns=1 +qr +showbadvers a.example >dig.out.test$n 2>&1 || ret=1
-  grep "; EDNS: version: 1, flags:; udp: 1232" dig.out.test$n >/dev/null || ret=1
-  grep "; EDNS: version: 0, flags:; udp: 1232" dig.out.test$n >/dev/null || ret=1
-  grep -F "status: BADVERS" dig.out.test$n >/dev/null || ret=1
-  grep -F "status: NOERROR" 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 +showtruncated works ($n)"
-  dig_with_opts @10.53.0.2 +qr +showtruncated truncated.example TXT >dig.out.test$n 2>&1 || ret=1
-  grep 'flags:[^;]* tc[ ;].*ANSWER: 0' dig.out.test$n >/dev/null || ret=1
-  grep 'ANSWER: 100,' dig.out.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)"
index 6b89877cc823dd894f8994a0b8ab639795143fea..e2868a499168da5c86e3398ef3123fbefdd2f466 100644 (file)
@@ -20,7 +20,7 @@ import re
 
 import pytest
 
-from digdelv.common import ARTIFACTS, check_ttl_range
+from digdelv.common import ARTIFACTS, check_ttl_range, needs_pyyaml, parse_yaml
 from isctest.util import param
 
 import isctest
@@ -39,6 +39,13 @@ def dig_fixture(named_port):
     return isctest.run.EnvCmd("DIG", f"-p {named_port}")
 
 
+def edns_yaml(text, direction="query"):
+    """Get the EDNS OPT pseudosection mapping of the first message in
+    dig +yaml output."""
+    message = parse_yaml(text)[0]["message"]
+    return message[f"{direction}_message_data"]["OPT_PSEUDOSECTION"]["EDNS"]
+
+
 def test_update_response(dig, ans6):
     """Check that dig rejects a response with the UPDATE opcode."""
     result = dig(
@@ -309,3 +316,374 @@ def test_nocrypto(dig, ns1):
     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
+
+
+def test_coflag(dig, ns3):
+    """Check that dig +coflag sets the EDNS CO flag in the sent query."""
+    result = dig(f"+tcp @{ns3.ip} +coflag +qr example")
+    assert Re(r"^; EDNS: version: 0, flags: co;") in result.out
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+@needs_pyyaml
+def test_coflag_yaml(dig, ns3):
+    """Check that dig +coflag +yaml shows the CO flag in the sent query."""
+    result = dig(f"+yaml +tcp @{ns3.ip} +coflag +qr example")
+    assert edns_yaml(result.out)["flags"] == "co"
+
+
+@pytest.mark.parametrize(
+    "option,sent_flags",
+    [
+        param("+raflag", "rd ra ad"),
+        param("+tcflag", "tc rd ad"),
+    ],
+)
+def test_header_flag_options(dig, ns3, option, sent_flags):
+    """Check that +raflag/+tcflag set the flag in the sent query and that
+    the response is unaffected."""
+    result = dig(f"+tcp @{ns3.ip} {option} +qr example")
+    assert Re(rf"^;; flags: {sent_flags}; QUERY: 1, ANSWER: 0") in result.out
+    assert Re(r"^;; flags: qr rd ra; QUERY: 1, ANSWER: 0,") in result.out
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+def test_zflag(dig, ns3):
+    """Check that dig +zflag sets the MBZ bit and that named ignores it."""
+    result = dig(f"+tcp @{ns3.ip} +zflag +qr A example")
+    assert Re(r"^;; flags: rd ad; MBZ: 0x4;") in result.out
+    assert Re(r"^;; flags: qr rd ra; QUERY: 1") in result.out
+    assert check_ttl_range(result.out, "SOA", 300)
+
+
+def test_ednsopt_08_no_insist(dig, ns3):
+    """Check that +qr +ednsopt=08 does not cause an INSIST failure."""
+    result = dig(f"@{ns3.ip} +ednsopt=08 +qr a a.example")
+    assert "INSIST" not in result.out
+    assert "FORMERR" in result.out
+
+
+@pytest.mark.parametrize(
+    "option",
+    [
+        param("3", id="number"),
+        param("nsid", id="name"),
+    ],
+)
+def test_ednsopt_nsid(dig, ns3, option):
+    """Check that +ednsopt accepts an option number as well as a name."""
+    result = dig(f"@{ns3.ip} +ednsopt={option} a.example")
+    assert Re(r'NSID: .* \("ns3"\)') in result.out
+    assert check_ttl_range(result.out, "A", 300)
+
+
+def test_ednsopt_update_lease(dig, ns3):
+    """Check that a single-lease UPDATE-LEASE option prints as expected."""
+    result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e10 +qr a.example")
+    assert "UPDATE-LEASE: 3600 (1 hour)" in result.out
+
+
+@needs_pyyaml
+def test_ednsopt_update_lease_yaml(dig, ns3):
+    """Check that a single-lease UPDATE-LEASE option prints as expected
+    with +yaml."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e10 +qr a.example")
+    assert edns_yaml(result.out)["UPDATE-LEASE"]["LEASE"] == 3600
+    assert "LEASE: 3600 # 1 hour" in result.out
+
+
+def test_ednsopt_update_lease_split(dig, ns3):
+    """Check that a split-lease UPDATE-LEASE option prints as expected."""
+    result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example")
+    assert "UPDATE-LEASE: 3600/1209600 (1 hour/2 weeks)" in result.out
+
+
+@needs_pyyaml
+def test_ednsopt_update_lease_split_yaml(dig, ns3):
+    """Check that a split-lease UPDATE-LEASE option prints as expected
+    with +yaml."""
+    result = dig(
+        f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example"
+    )
+    update_lease = edns_yaml(result.out)["UPDATE-LEASE"]
+    assert update_lease["LEASE"] == 3600
+    assert update_lease["KEY-LEASE"] == 1209600
+    assert "LEASE: 3600 # 1 hour" in result.out
+    assert "KEY-LEASE: 1209600 # 2 weeks" in result.out
+
+
+def test_ednsopt_llq(dig, ns3):
+    """Check that the LLQ option prints as expected."""
+    result = dig(
+        f"@{ns3.ip} +ednsopt=llq:0001000200001234567812345678fefefefe +qr a.example"
+    )
+    pattern = (
+        r"LLQ: Version: 1, Opcode: 2, Error: 0, "
+        r"Identifier: 1311768465173141112, Lifetime: 4278124286$"
+    )
+    assert Re(pattern) in result.out
+
+
+@needs_pyyaml
+def test_ednsopt_llq_yaml(dig, ns3):
+    """Check that the LLQ option prints as expected with +yaml."""
+    result = dig(
+        f"@{ns3.ip} +yaml +ednsopt=llq:0001000200001234567812345678fefefefe "
+        "+qr a.example"
+    )
+    llq = edns_yaml(result.out)["LLQ"]
+    assert llq["LLQ-VERSION"] == 1
+    assert llq["LLQ-OPCODE"] == 2
+    assert llq["LLQ-ERROR"] == 0
+    assert llq["LLQ-ID"] == 1311768465173141112
+    assert llq["LLQ-LEASE"] == 4278124286
+
+
+def test_ednsopt_key_tag_empty(dig, ns3):
+    """Check that an empty key-tag option is sent and FORMERR is returned."""
+    result = dig(f"@{ns3.ip} +ednsopt=key-tag a.example +qr")
+    assert Re(r"; KEY-TAG: *$") in result.out
+    assert "status: FORMERR" in result.out
+
+
+def test_ednsopt_key_tag(dig, ns3):
+    """Check that a key-tag value list is sent and accepted."""
+    result = dig(f"@{ns3.ip} +ednsopt=key-tag:00010002 a.example +qr")
+    assert Re(r"; KEY-TAG: 1, 2$") in result.out
+    assert "status: FORMERR" not in result.out
+    assert check_ttl_range(result.out, "A", 300)
+
+
+@needs_pyyaml
+def test_ednsopt_key_tag_yaml(dig, ns3):
+    """Check that a key-tag value list prints as a list with +yaml."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt=key-tag:00010002 a.example +qr")
+    assert edns_yaml(result.out)["KEY-TAG"] == [1, 2]
+
+
+def test_ednsopt_key_tag_malformed(dig, ns3):
+    """Check that a malformed key-tag value list is sent as raw data and
+    FORMERR is returned."""
+    result = dig(f"@{ns3.ip} +ednsopt=key-tag:0001000201 a.example +qr")
+    assert "; KEY-TAG: 00 01 00 02 01" in result.out
+    assert "status: FORMERR" in result.out
+
+
+@pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
+def test_ednsopt_tag(dig, ns3, tag):
+    """Check that a valid client/server-tag value is sent and accepted."""
+    result = dig(f"@{ns3.ip} +ednsopt={tag}:0001 a.example +qr")
+    assert Re(rf"; {tag.upper()}: 1$") in result.out
+    assert "status: FORMERR" not in result.out
+
+
+@needs_pyyaml
+@pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
+def test_ednsopt_tag_yaml(dig, ns3, tag):
+    """Check that a client/server-tag value prints as expected with +yaml."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt={tag}:0001 a.example +qr")
+    assert edns_yaml(result.out)[tag.upper()] == 1
+
+
+@pytest.mark.parametrize(
+    "value",
+    [
+        param("01", id="too-short"),
+        param("000001", id="too-long"),
+    ],
+)
+@pytest.mark.parametrize("tag", ["client-tag", "server-tag"])
+def test_ednsopt_tag_bad_length(dig, ns3, tag, value):
+    """Check that FORMERR is returned for a client/server-tag value of the
+    wrong length."""
+    result = dig(f"@{ns3.ip} +ednsopt={tag}:{value} a.example +qr")
+    assert f"; {tag.upper()}" in result.out
+    assert "status: FORMERR" in result.out
+
+
+def test_ednsopt_chain(dig, ns3):
+    """Check that the CHAIN option prints special characters escaped."""
+    result = dig(rf'@{ns3.ip} +ednsopt=chain:02002200 a.\000" +qr')
+    assert r'; CHAIN: "\000\""' in result.out
+
+
+@needs_pyyaml
+def test_ednsopt_chain_yaml(dig, ns3):
+    """Check that the CHAIN option prints special characters escaped
+    with +yaml."""
+    result = dig(rf'@{ns3.ip} +yaml +ednsopt=chain:02002200 a.\000" +qr')
+    assert edns_yaml(result.out)["CHAIN"] == r"\000\""
+
+
+def test_expire(dig, ns1):
+    """Check that dig processes +expire."""
+    result = dig(f"@{ns1.ip} +expire . soa")
+    assert "; EXPIRE: 1200 (20 minutes)" in result.out
+
+
+@needs_pyyaml
+def test_expire_yaml(dig, ns1):
+    """Check that dig processes +expire with +yaml."""
+    result = dig(f"@{ns1.ip} +yaml +expire . soa")
+    assert edns_yaml(result.out, "response")["EXPIRE"] == 1200
+    assert "EXPIRE: 1200 # 20 minutes" in result.out
+
+
+def test_keepalive(dig, ns1):
+    """Check that dig processes +keepalive."""
+    result = dig(f"@{ns1.ip} +keepalive . soa +tcp")
+    assert "; TCP-KEEPALIVE: 30.0 secs" in result.out
+
+
+@needs_pyyaml
+def test_keepalive_yaml(dig, ns1):
+    """Check that dig processes +keepalive with +yaml."""
+    result = dig(f"@{ns1.ip} +yaml +keepalive . soa +tcp")
+    assert edns_yaml(result.out, "response")["TCP-KEEPALIVE"] == "30.0 secs"
+
+
+@pytest.mark.parametrize(
+    "payload,expected",
+    [
+        param("ede:0000666f6f", "; EDE: 0 (Other): (foo)", id="first-defined-code"),
+        param("ede:0018", "; EDE: 24 (Invalid Data)", id="last-defined-code"),
+        param("ede:0019666f6f", "; EDE: 25: (foo)", id="undefined-code"),
+        param("ede", "; EDE:", id="empty"),
+        param("ede:00", '; EDE: 00 (".")', id="too-short"),
+    ],
+)
+def test_ednsopt_ede(dig, ns3, payload, expected):
+    """Check that Extended DNS Error options, including invalid ones with
+    a too short payload, are printed correctly."""
+    result = dig(f"@{ns3.ip} +ednsopt={payload} a.example +qr")
+    assert Re("^" + re.escape(expected) + "$") in result.out
+
+
+@needs_pyyaml
+@pytest.mark.parametrize(
+    "payload,info_code,extra_text",
+    [
+        param("ede:0000666f6f", "0 (Other)", "foo", id="first-defined-code"),
+        param("ede:0018", "24 (Invalid Data)", None, id="last-defined-code"),
+        param("ede:0019666f6f", 25, "foo", id="undefined-code"),
+    ],
+)
+def test_ednsopt_ede_yaml(dig, ns3, payload, info_code, extra_text):
+    """Check that Extended DNS Error options are printed correctly
+    with +yaml."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr")
+    ede = edns_yaml(result.out)["EDE"]
+    assert ede["INFO-CODE"] == info_code
+    if extra_text is None:
+        assert "EXTRA-TEXT" not in ede
+    else:
+        assert ede["EXTRA-TEXT"] == extra_text
+
+
+@needs_pyyaml
+def test_ednsopt_ede_yaml_specials(dig, ns3):
+    """Check that EDE extra text with '"' and '\\' specials survives YAML
+    quoting."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt=ede:0000666f6f225c a.example +qr")
+    assert edns_yaml(result.out)["EDE"]["EXTRA-TEXT"] == 'foo"\\'
+
+
+@needs_pyyaml
+@pytest.mark.parametrize(
+    "payload,expected",
+    [
+        param("ede", None, id="empty"),
+        param("ede:00", '00 (".")', id="too-short"),
+    ],
+)
+def test_ednsopt_ede_yaml_invalid(dig, ns3, payload, expected):
+    """Check that invalid Extended DNS Error options with a too short
+    payload are printed correctly with +yaml."""
+    result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr")
+    assert edns_yaml(result.out)["EDE"] == expected
+
+
+def test_ednsopt_malformed(dig, ns3):
+    """Check that dig handles the malformed option '+ednsopt=:'
+    gracefully."""
+    result = dig(f"@{ns3.ip} +ednsopt=: a.example", raise_on_exception=False)
+    assert result.rc != 0
+    assert "ednsopt no code point specified" in result.err
+
+
+def test_zoneversion(dig, ns2):
+    """Check +zoneversion against an authoritative server."""
+    result = dig(f"@{ns2.ip} +zoneversion a.example")
+    assert "; ZONEVERSION: ZONE: example, SOA-SERIAL: 2000042407" in result.out
+
+
+def test_zoneversion_disabled(dig, ns2):
+    """Check +zoneversion against an authoritative server with zoneversion
+    disabled."""
+    result = dig(f"@{ns2.ip} +zoneversion a.example.tld")
+    assert "status: NOERROR" in result.out
+    assert "; ZONEVERSION:" not in result.out
+
+
+@needs_pyyaml
+def test_zoneversion_yaml(dig, ns2):
+    """Check +yaml +zoneversion against an authoritative server."""
+    result = dig(f"@{ns2.ip} +yaml +zoneversion a.example")
+    assert edns_yaml(result.out, "response")["ZONEVERSION"] == {
+        "ZONE": "example",
+        "SOA-SERIAL": 2000042407,
+    }
+
+
+def test_ednsopt_zoneversion(dig, ns2):
+    """Check that a ZONEVERSION option sent in a query returns FORMERR."""
+    result = dig(f"@{ns2.ip} +ednsopt=ZONEVERSION:0100000007DA a.example")
+    assert "status: FORMERR" in result.out
+    assert "; ZONEVERSION:" not in result.out
+
+
+def test_zoneversion_recursive(dig, ns3):
+    """Check that +zoneversion via a recursive server returns no
+    ZONEVERSION."""
+    result = dig(f"@{ns3.ip} +zoneversion a.example")
+    assert "; ZONEVERSION:" not in result.out
+
+
+def test_zoneversion_non_serial(dig, ns2):
+    """Check the display of a non SOA-SERIAL type zoneversion."""
+    result = dig(f"@{ns2.ip} +qr +ednsopt=ZONEVERSION:0100000007DA a.example")
+    assert (
+        '; ZONEVERSION: LABELS: 1, TYPE: 0, VALUE: 000007da ("....")' not in result.out
+    )
+
+
+@needs_pyyaml
+def test_zoneversion_non_serial_yaml(dig, ns2):
+    """Check the display of a non SOA-SERIAL type zoneversion with +yaml."""
+    result = dig(f"@{ns2.ip} +qr +ednsopt=ZONEVERSION:0100000007DA a.example +yaml")
+    assert "ZONEVERSION" in edns_yaml(result.out)
+
+
+def test_ednsflags_reenables_edns(dig, ns3):
+    """Check that +noedns +ednsflags=<nonzero> re-enables EDNS."""
+    result = dig(f"@{ns3.ip} +qr +noedns +ednsflags=0x70 a.example")
+    assert "; EDNS: version: 0, flags:; MBZ: 0x0070, udp: 1232" in result.out
+    assert "; EDNS: version: 0, flags:; udp: 1232" in result.out
+
+
+def test_showbadvers(dig, ns3):
+    """Check that +showbadvers displays the BADVERS response as well as
+    the retry without EDNS version 1."""
+    result = dig(f"@{ns3.ip} +edns=1 +qr +showbadvers a.example")
+    assert "; EDNS: version: 1, flags:; udp: 1232" in result.out
+    assert "; EDNS: version: 0, flags:; udp: 1232" in result.out
+    assert "status: BADVERS" in result.out
+    assert "status: NOERROR" in result.out
+
+
+def test_showtruncated(dig, ns2):
+    """Check that +showtruncated displays the truncated response as well
+    as the TCP retry."""
+    result = dig(f"@{ns2.ip} +qr +showtruncated truncated.example TXT")
+    assert Re(r"flags:[^;]* tc[ ;].*ANSWER: 0") in result.out
+    assert "ANSWER: 100," in result.out