ipa/0161-ipatests-Add-DNS-functional-integration-tests.patch
2026-04-22 11:51:31 -04:00

1280 lines
50 KiB
Diff

From b2c83c462aba3d7367f01f665f14b126b7e74b9e Mon Sep 17 00:00:00 2001
From: PRANAV THUBE <pthube@redhat.com>
Date: Wed, 31 Dec 2025 12:15:51 +0530
Subject: [PATCH] ipatests: Add DNS functional integration tests.
Add tests covering DNS zone management and all record types:
A, AAAA, AFSDB, CNAME, TXT, SRV, MX, PTR, NAPTR, DNAME, CERT, LOC, KX
Including zone permissions and negative test cases.
Related: https://pagure.io/freeipa/issue/9911
Reviewed-By: Florence Blanc-Renaud <frenaud@redhat.com>
Reviewed-By: Rafael Guterres Jeffman <rjeffman@redhat.com>
Reviewed-By: David Hanina <dhanina@redhat.com>
---
ipatests/pytest_ipa/integration/tasks.py | 126 +++-
ipatests/test_integration/test_dns.py | 707 +++++++++++++++++++++++
ipatests/test_xmlrpc/test_dns_plugin.py | 288 ++++++++-
3 files changed, 1113 insertions(+), 8 deletions(-)
diff --git a/ipatests/pytest_ipa/integration/tasks.py b/ipatests/pytest_ipa/integration/tasks.py
index 7def8fe3962487f28999cb975aff512cd36331fe..32ac5cbc2c6fe87850dfb15c1d5beae6fa648dfb 100755
--- a/ipatests/pytest_ipa/integration/tasks.py
+++ b/ipatests/pytest_ipa/integration/tasks.py
@@ -1628,6 +1628,71 @@ def add_a_record(master, host):
'--a-rec', host.ip])
+def add_dns_record(master, zone, name, record_type, record_value,
+ raiseonerr=True):
+ """Add DNS record of any type.
+
+ :param master: The IPA master host to run command on
+ :param zone: DNS zone name (e.g., 'example.com.')
+ :param name: Record name (e.g., 'www' or '@' for zone apex)
+ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname',
+ 'txt', 'srv', 'mx', 'ptr', 'naptr', 'dname',
+ 'cert', 'loc', 'kx', etc.
+ :param record_value: List of values for the record
+ :param raiseonerr: If True, raise exception on command failure
+ :return: Command result object
+ """
+ command = ['ipa', 'dnsrecord-add', zone, name]
+ opt = f'--{record_type}-rec'
+ for val in record_value:
+ command.extend([opt, val])
+ return master.run_command(command, raiseonerr=raiseonerr)
+
+
+def del_dns_record(master, zone, name, record_type=None, record_value=None,
+ del_all=False, raiseonerr=True):
+ """Delete DNS record of any type.
+
+ :param record_type: Record type like 'a', 'aaaa', 'afsdb', 'cname', etc.
+ :param record_value: List of values (optional)
+ :param del_all: If True, delete all records for this name
+ """
+ command = ['ipa', 'dnsrecord-del', zone, name]
+ if del_all:
+ command.append('--del-all')
+ elif record_type and record_value:
+ opt = f'--{record_type}-rec'
+ for val in record_value:
+ command.extend([opt, val])
+ return master.run_command(command, raiseonerr=raiseonerr)
+
+
+def find_dns_record(master, zone, name=None, raiseonerr=True):
+ """Find DNS record.
+
+ :param name: Record name, if not provided searches all records in zone
+ """
+ command = ['ipa', 'dnsrecord-find', zone]
+ if name is not None:
+ command.append(name)
+ return master.run_command(command, raiseonerr=raiseonerr)
+
+
+def mod_dns_record(master, zone, name, record_type, old_value, new_value,
+ raiseonerr=True):
+ """Modify DNS record value.
+
+ :param record_type: Record type like 'a', 'aaaa', 'txt', etc.
+ :param old_value: Current record value
+ :param new_value: New record value
+ """
+ return master.run_command([
+ 'ipa', 'dnsrecord-mod', zone, name,
+ f'--{record_type}-rec={old_value}',
+ f'--{record_type}-data={new_value}'
+ ], raiseonerr=raiseonerr)
+
+
def resolve_record(nameserver, query, rtype="SOA", retry=True, timeout=100):
"""Resolve DNS record
:retry: if resolution failed try again until timeout is reached
@@ -2002,7 +2067,9 @@ def ldappasswd_sysaccount_change(user, oldpw, newpw, master, use_dirman=False):
def add_dns_zone(master, zone, skip_overlap_check=False,
- dynamic_update=False, add_a_record_hosts=None):
+ dynamic_update=False, add_a_record_hosts=None,
+ admin_email=None, refresh=None, retry=None,
+ expire=None, minimum=None, ttl=None, raiseonerr=True):
"""
Add DNS zone if it is not already added.
"""
@@ -2010,14 +2077,27 @@ def add_dns_zone(master, zone, skip_overlap_check=False,
result = master.run_command(
['ipa', 'dnszone-show', zone], raiseonerr=False)
+ # Verify both return code and zone name before adding
if result.returncode != 0:
command = ['ipa', 'dnszone-add', zone]
if skip_overlap_check:
command.append('--skip-overlap-check')
if dynamic_update:
command.append('--dynamic-update=True')
-
- master.run_command(command)
+ if admin_email:
+ command.append('--admin-email=' + admin_email)
+ if refresh:
+ command.append('--refresh=' + str(refresh))
+ if retry:
+ command.append('--retry=' + str(retry))
+ if expire:
+ command.append('--expire=' + str(expire))
+ if minimum:
+ command.append('--minimum=' + str(minimum))
+ if ttl:
+ command.append('--ttl=' + str(ttl))
+
+ master.run_command(command, raiseonerr=raiseonerr)
if add_a_record_hosts:
for host in add_a_record_hosts:
@@ -2027,6 +2107,46 @@ def add_dns_zone(master, zone, skip_overlap_check=False,
logger.debug('Zone %s already added.', zone)
+def del_dns_zone(host, zone, raiseonerr=False):
+ """Delete DNS zone."""
+ return host.run_command(
+ ['ipa', 'dnszone-del', zone], raiseonerr=raiseonerr)
+
+
+def find_dns_zone(host, zone, all_attrs=False, raiseonerr=True):
+ """Find DNS zone."""
+ command = ['ipa', 'dnszone-find', zone]
+ if all_attrs:
+ command.append('--all')
+ return host.run_command(command, raiseonerr=raiseonerr)
+
+
+def show_dns_zone(host, zone, all_attrs=False, raiseonerr=True):
+ """Show DNS zone."""
+ command = ['ipa', 'dnszone-show', zone]
+ if all_attrs:
+ command.append('--all')
+ return host.run_command(command, raiseonerr=raiseonerr)
+
+
+def add_dns_zone_permission(host, zone, raiseonerr=True):
+ """Add permission to manage DNS zone."""
+ return host.run_command(['ipa', 'dnszone-add-permission', zone],
+ raiseonerr=raiseonerr)
+
+
+def remove_dns_zone_permission(host, zone, raiseonerr=True):
+ """Remove permission to manage DNS zone."""
+ return host.run_command(['ipa', 'dnszone-remove-permission', zone],
+ raiseonerr=raiseonerr)
+
+
+def find_permission(host, permission, raiseonerr=True):
+ """Find permission."""
+ return host.run_command(['ipa', 'permission-find', permission],
+ raiseonerr=raiseonerr)
+
+
def sign_ca_and_transport(host, csr_name, root_ca_name, ipa_ca_name,
root_ca_path_length=None, ipa_ca_path_length=1,
key_size=None, root_ca_extensions=()):
diff --git a/ipatests/test_integration/test_dns.py b/ipatests/test_integration/test_dns.py
index 1cef22525b690050aa1230733f115cd7f4099c53..905227497c8879bfe4cf8b027e396689c0451208 100644
--- a/ipatests/test_integration/test_dns.py
+++ b/ipatests/test_integration/test_dns.py
@@ -5,9 +5,70 @@
from __future__ import absolute_import
+import time
+
+import dns.resolver
+from ipapython.dnsutil import DNSResolver
from ipatests.pytest_ipa.integration import tasks
from ipatests.test_integration.base import IntegrationTest
+# =============================================================================
+# DNS Test Constants
+# =============================================================================
+# Test zone configuration
+ZONE = "testzone"
+EMAIL = "ipaqar.redhat.com"
+REFRESH = 303
+RETRY = 101
+EXPIRE = 1202
+MINIMUM = 33
+TTL = 55
+
+# A record values
+A_RECORD = "1.2.3.4"
+MULTI_A_RECORD1 = "1.2.3.4"
+MULTI_A_RECORD2 = "2.3.4.5"
+
+# AAAA record values
+AAAA = "fec0:0:a10:6000:10:16ff:fe98:193"
+AAAA_BAD1 = "bada:aaaa:real:ly:bad:dude:extr:a"
+AAAA_BAD2 = "aaaa:bbbb:cccc:dddd:eeee:fffff"
+
+# Other record types
+AFSDB = "green.femto.edu."
+CNAME = "m.l.k."
+TXT = "none=1.2.3.4"
+SRV_A = "0 100 389"
+SRV = "why.go.here.com."
+NAPTR = '100 10 U E2U+msg !^.*$!mailto:info@example.com! .'
+NAPTR_FIND = "info@example.com"
+DNAME = f"bar.{ZONE}."
+DNAME2 = f"bar_underscore.{ZONE}."
+CERT_B = "1 1 1"
+CERT = "F835EDA21E94B565716F"
+LOC = "37 23 30.900 N 121 59 19.000 W 7.00m 100.00m 100.00m 2.00m"
+
+# KX records
+KX_PREF1 = "1234"
+KX_BAD_PREF1 = "-1"
+KX_BAD_PREF2 = "123345678"
+
+# PTR zone configuration
+PTR_OCTET = "4.4.4"
+PTR_ZONE = f"{PTR_OCTET}.in-addr.arpa."
+PTR = "8"
+PTR_VALUE = "in.awesome.domain."
+PTR_EMAIL = "ipaqar.redhat.com"
+PTR_REFRESH = 393
+PTR_RETRY = 191
+PTR_EXPIRE = 1292
+PTR_MINIMUM = 39
+PTR_TTL = 59
+
+# Persistent search test values
+NEW_TXT = "newip=5.6.7.8"
+NEWER_TXT = "newip=8.7.6.5"
+
class TestDNS(IntegrationTest):
"""Tests for DNS feature.
@@ -38,3 +99,649 @@ class TestDNS(IntegrationTest):
cmd = self.master.run_command(['dig', '+short', '-t', 'SOA',
self.master.domain.name])
assert 'fake' not in cmd.stdout_text
+
+
+class TestDNSAcceptance(IntegrationTest):
+ """DNS Acceptance tests.
+
+ This test class covers all the DNS acceptance tests including
+ zone management, record types (A, AAAA, AFSDB, CNAME, TXT, SRV, MX,
+ PTR, NAPTR, DNAME, CERT, LOC, KX), zone permissions, and persistent
+ search functionality.
+
+ Converted from bash test script t.dns.sh
+ """
+ topology = 'line'
+ num_replicas = 0
+
+ @classmethod
+ def install(cls, mh):
+ super(TestDNSAcceptance, cls).install(mh)
+ # Set domain-dependent values
+ cls.MANAGED_ZONE = f"qa.{cls.master.domain.name}"
+ cls.MANAGED_ZONE1 = f"dev.{cls.master.domain.name}"
+ cls.NONEXISTENT_ZONE = f"nonexistent.{cls.master.domain.name}"
+ cls.ZONE_PSEARCH = f"westford.{cls.master.domain.name}"
+ cls.MX = f"mail.{cls.master.domain.name}"
+ cls.A_HOST = f"1.{cls.master.domain.name}"
+ # Setup DNS resolver for test queries
+ cls.resolver = DNSResolver()
+ cls.resolver.nameservers = [cls.master.ip]
+ cls.resolver.lifetime = 10
+
+ @classmethod
+ def uninstall(cls, mh):
+ # Cleanup zones if they exist
+ tasks.kinit_admin(cls.master)
+ for zone in [ZONE, PTR_ZONE, cls.MANAGED_ZONE,
+ cls.MANAGED_ZONE1, cls.ZONE_PSEARCH]:
+ if zone:
+ cls.master.run_command(
+ ['ipa', 'dnszone-del', zone], raiseonerr=False
+ )
+ super(TestDNSAcceptance, cls).uninstall(mh)
+
+ # =========================================================================
+ # DNS Zone Tests
+ # =========================================================================
+
+ def test_dns_zone(self):
+ """Test DNS zone creation, verification, and dig queries.
+
+ This test covers zone management operations including creating zones
+ with valid and invalid parameters, and verifying zone attributes
+ via IPA and dig.
+ """
+ tasks.kinit_admin(self.master)
+
+ # Create a new DNS zone with all SOA parameters
+ tasks.add_dns_zone(
+ self.master, ZONE,
+ admin_email=EMAIL,
+ refresh=REFRESH,
+ retry=RETRY,
+ expire=EXPIRE,
+ minimum=MINIMUM,
+ ttl=TTL
+ )
+ # Verify the new zone was created and is findable
+ result = tasks.find_dns_zone(self.master, ZONE, all_attrs=True)
+ assert ZONE in result.stdout_text
+
+ # Verify DNS server returns correct SOA attributes using DNS API
+ self.master.run_command(['ipactl', 'restart'])
+ time.sleep(5)
+ soa = self.resolver.resolve(ZONE, 'SOA')[0]
+ assert self.master.hostname in str(soa.mname)
+ assert EMAIL.replace('@', '.') in str(soa.rname)
+ assert soa.refresh == REFRESH
+ assert soa.retry == RETRY
+ assert soa.expire == EXPIRE
+ assert soa.minimum == MINIMUM
+
+ # =========================================================================
+ # A Record Tests
+ # =========================================================================
+
+ def test_a_record(self):
+ """Test A record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # Single A record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'allll',
+ record_type='a', record_value=[A_RECORD])
+ ans = self.resolver.resolve(f'allll.{ZONE}', 'A')
+ assert A_RECORD in [r.address for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'allll',
+ record_type='a', record_value=[A_RECORD])
+ try:
+ self.resolver.resolve(f'allll.{ZONE}', 'A')
+ raise AssertionError(
+ f"Resolving allll.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # Multiple A records: add, verify, delete, verify deleted
+ multi_recs = [MULTI_A_RECORD1, MULTI_A_RECORD2]
+ tasks.add_dns_record(self.master, ZONE, 'aa2',
+ record_type='a', record_value=multi_recs)
+ ans = self.resolver.resolve(f'aa2.{ZONE}', 'A')
+ assert MULTI_A_RECORD1 in [r.address for r in ans]
+ assert MULTI_A_RECORD2 in [r.address for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'aa2',
+ record_type='a', record_value=multi_recs)
+ try:
+ self.resolver.resolve(f'aa2.{ZONE}', 'A')
+ raise AssertionError(
+ f"Resolving aa2.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Records deleted, name no longer exists - expected
+
+ # =========================================================================
+ # AAAA Record Tests
+ # =========================================================================
+
+ def test_aaaa_record(self):
+ """Test AAAA record add, verify, delete, and invalid values."""
+ tasks.kinit_admin(self.master)
+
+ # AAAA record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'aaaa',
+ record_type='aaaa', record_value=[AAAA])
+ ans = self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA')
+ assert AAAA in [r.address for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'aaaa',
+ record_type='aaaa', record_value=[AAAA])
+ try:
+ self.resolver.resolve(f'aaaa.{ZONE}', 'AAAA')
+ raise AssertionError(
+ f"Resolving aaaa.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # Invalid AAAA record should fail and not be created
+ result = tasks.add_dns_record(self.master, ZONE, 'aaaab',
+ record_type='aaaa',
+ record_value=[AAAA_BAD1],
+ raiseonerr=False)
+ assert result.returncode != 0
+ try:
+ self.resolver.resolve(f'aaaab.{ZONE}', 'AAAA')
+ raise AssertionError(
+ f"Resolving aaaab.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record was never created - expected
+
+ # =========================================================================
+ # AFSDB Record Tests
+ # =========================================================================
+
+ def test_afsdb_record(self):
+ """Test AFSDB record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # AFSDB record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'afsdb',
+ record_type='afsdb',
+ record_value=[f'0 {AFSDB}'])
+ ans = self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB')
+ assert AFSDB in [str(r.hostname) for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'afsdb',
+ record_type='afsdb',
+ record_value=[f'0 {AFSDB}'])
+ try:
+ self.resolver.resolve(f'afsdb.{ZONE}', 'AFSDB')
+ raise AssertionError(
+ f"Resolving afsdb.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # CNAME Record Tests
+ # =========================================================================
+
+ def test_cname_record(self):
+ """Test CNAME record add, verify, delete, and duplicate (bz915807)."""
+ tasks.kinit_admin(self.master)
+
+ # CNAME record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'cname',
+ record_type='cname', record_value=[CNAME])
+ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME')
+ assert CNAME in [str(r.target) for r in ans]
+
+ # Duplicate CNAME should fail and not be created (bz915807)
+ result = tasks.add_dns_record(self.master, ZONE, 'cname',
+ record_type='cname',
+ record_value=['a.b.c'], raiseonerr=False)
+ assert result.returncode != 0
+ ans = self.resolver.resolve(f'cname.{ZONE}', 'CNAME')
+ assert 'a.b.c' not in [str(r.target) for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'cname',
+ record_type='cname', record_value=[CNAME])
+ try:
+ self.resolver.resolve(f'cname.{ZONE}', 'CNAME')
+ raise AssertionError(
+ f"Resolving cname.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # TXT Record Tests
+ # =========================================================================
+
+ def test_txt_record(self):
+ """Test TXT record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # TXT record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'txt',
+ record_type='txt', record_value=[TXT])
+ ans = self.resolver.resolve(f'txt.{ZONE}', 'TXT')
+ assert any(TXT in str(r) for r in ans)
+
+ tasks.del_dns_record(self.master, ZONE, 'txt',
+ record_type='txt', record_value=[TXT])
+ try:
+ self.resolver.resolve(f'txt.{ZONE}', 'TXT')
+ raise AssertionError(
+ f"Resolving txt.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # SRV Record Tests
+ # =========================================================================
+
+ def test_srv_record(self):
+ """Test SRV record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # SRV record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, '_srv',
+ record_type='srv',
+ record_value=[f'{SRV_A} {SRV}'])
+ ans = self.resolver.resolve(f'_srv.{ZONE}', 'SRV')
+ assert SRV in [str(r.target) for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, '_srv', del_all=True)
+ try:
+ self.resolver.resolve(f'_srv.{ZONE}', 'SRV')
+ raise AssertionError(
+ f"Resolving _srv.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # MX Record Tests
+ # =========================================================================
+
+ def test_mx_record(self):
+ """Test MX record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # MX record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, '@',
+ record_type='mx',
+ record_value=[f'10 {self.MX}.'])
+ ans = self.resolver.resolve(ZONE, 'MX')
+ assert f'{self.MX}.' in [str(r.exchange) for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, '@',
+ record_type='mx',
+ record_value=[f'10 {self.MX}.'])
+ try:
+ self.resolver.resolve(ZONE, 'MX')
+ raise AssertionError(
+ f"Resolving MX for {ZONE} should have raised NoAnswer")
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ pass # Record deleted or no MX records left - expected
+
+ # =========================================================================
+ # PTR Zone Tests
+ # =========================================================================
+ def test_ptr_zone(self):
+ """Test PTR zone creation and verification.
+
+ This test covers PTR zone management including creating zone
+ with all SOA parameters, and verifying attributes via IPA and dig.
+ """
+ tasks.kinit_admin(self.master)
+ # Clean up if zone exists
+ tasks.del_dns_zone(self.master, PTR_ZONE)
+
+ # Create PTR zone with all SOA parameters
+ tasks.add_dns_zone(
+ self.master, PTR_ZONE,
+ skip_overlap_check=True,
+ admin_email=PTR_EMAIL,
+ refresh=PTR_REFRESH,
+ retry=PTR_RETRY,
+ expire=PTR_EXPIRE,
+ minimum=PTR_MINIMUM,
+ ttl=PTR_TTL
+ )
+
+ # Verify PTR zone gets created with the correct attributes
+ result = tasks.find_dns_zone(
+ self.master, PTR_ZONE, all_attrs=True)
+ assert PTR_ZONE in result.stdout_text
+
+ # Verify PTR zone SOA attributes using DNS resolver
+ soa = self.resolver.resolve(PTR_ZONE, 'SOA')[0]
+ assert self.master.hostname in str(soa.mname)
+ assert PTR_EMAIL.replace('@', '.') in str(soa.rname)
+ assert soa.refresh == PTR_REFRESH
+ assert soa.retry == PTR_RETRY
+ assert soa.expire == PTR_EXPIRE
+ assert soa.minimum == PTR_MINIMUM
+
+ # =========================================================================
+ # PTR Record Tests
+ # =========================================================================
+
+ def test_ptr_record(self):
+ """Test PTR record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # PTR record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, PTR_ZONE, PTR,
+ record_type='ptr', record_value=[PTR_VALUE])
+ ans = self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR')
+ assert PTR_VALUE in [str(r.target) for r in ans]
+
+ tasks.del_dns_record(self.master, PTR_ZONE, PTR,
+ record_type='ptr', record_value=[PTR_VALUE])
+ try:
+ self.resolver.resolve(f'{PTR}.{PTR_ZONE}', 'PTR')
+ raise AssertionError(
+ f"Resolving {PTR}.{PTR_ZONE} should have raised "
+ "NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # NAPTR Record Tests
+ # =========================================================================
+
+ def test_naptr_record(self):
+ """Test NAPTR record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # NAPTR record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'naptr',
+ record_type='naptr', record_value=[NAPTR])
+ ans = self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR')
+ assert any(NAPTR_FIND in str(r.regexp) for r in ans)
+
+ tasks.del_dns_record(self.master, ZONE, 'naptr',
+ record_type='naptr', record_value=[NAPTR])
+ try:
+ self.resolver.resolve(f'naptr.{ZONE}', 'NAPTR')
+ raise AssertionError(
+ f"Resolving naptr.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted, name no longer exists - expected
+
+ # =========================================================================
+ # DNAME Record Tests
+ # =========================================================================
+
+ def test_dname_record(self):
+ """Test DNAME record add, verify, delete, and underscore (bz915797)."""
+ tasks.kinit_admin(self.master)
+
+ # DNAME record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'dname',
+ record_type='dname', record_value=[DNAME])
+ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME')
+ assert DNAME in [str(r.target) for r in ans]
+
+ # Duplicate DNAME should fail (bz915797)
+ result = tasks.add_dns_record(self.master, ZONE, 'dname',
+ record_type='dname',
+ record_value=[DNAME2],
+ raiseonerr=False)
+ assert result.returncode != 0
+
+ tasks.del_dns_record(self.master, ZONE, 'dname',
+ record_type='dname', record_value=[DNAME])
+ try:
+ self.resolver.resolve(f'dname.{ZONE}', 'DNAME')
+ raise AssertionError(
+ f"Resolving dname.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted - expected
+
+ # DNAME with underscore: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'dname',
+ record_type='dname', record_value=[DNAME2])
+ ans = self.resolver.resolve(f'dname.{ZONE}', 'DNAME')
+ assert DNAME2 in [str(r.target) for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, 'dname',
+ record_type='dname', record_value=[DNAME2])
+ try:
+ self.resolver.resolve(f'dname.{ZONE}', 'DNAME')
+ raise AssertionError(
+ f"Resolving dname.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted - expected
+
+ # =========================================================================
+ # CERT Record Tests
+ # =========================================================================
+
+ def test_cert_record(self):
+ """Test CERT record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # CERT record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, 'cert',
+ record_type='cert',
+ record_value=[f'{CERT_B} {CERT}'])
+ ans = self.resolver.resolve(f'cert.{ZONE}', 'CERT')
+ assert any(CERT in str(r) for r in ans)
+
+ tasks.del_dns_record(self.master, ZONE, 'cert',
+ record_type='cert',
+ record_value=[f'{CERT_B} {CERT}'])
+ try:
+ self.resolver.resolve(f'cert.{ZONE}', 'CERT')
+ raise AssertionError(
+ f"Resolving cert.{ZONE} should have raised NXDOMAIN")
+ except dns.resolver.NXDOMAIN:
+ pass # Record deleted - expected
+
+ # =========================================================================
+ # LOC Record Tests
+ # =========================================================================
+
+ def test_loc_record(self):
+ """Test LOC record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # LOC record: add, verify, delete, verify deleted
+ tasks.add_dns_record(self.master, ZONE, '@',
+ record_type='loc', record_value=[LOC])
+ ans = self.resolver.resolve(ZONE, 'LOC')
+ assert any(LOC in str(r) for r in ans)
+
+ tasks.del_dns_record(self.master, ZONE, '@',
+ record_type='loc', record_value=[LOC])
+ try:
+ self.resolver.resolve(ZONE, 'LOC')
+ raise AssertionError(
+ f"Resolving LOC for {ZONE} should have raised NoAnswer")
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ pass # Record deleted - expected
+
+ # =========================================================================
+ # KX Record Tests
+ # =========================================================================
+
+ def test_kx_record(self):
+ """Test KX record add, verify, and delete operations."""
+ tasks.kinit_admin(self.master)
+
+ # KX record: add, verify, delete, verify deleted
+ kx_val = f'{KX_PREF1} {self.A_HOST}'
+ tasks.add_dns_record(self.master, ZONE, '@',
+ record_type='kx', record_value=[kx_val])
+ ans = self.resolver.resolve(ZONE, 'KX')
+ assert int(KX_PREF1) in [r.preference for r in ans]
+
+ tasks.del_dns_record(self.master, ZONE, '@',
+ record_type='kx', record_value=[kx_val])
+ try:
+ self.resolver.resolve(ZONE, 'KX')
+ raise AssertionError(
+ f"Resolving KX for {ZONE} should have raised NoAnswer")
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ pass # Record deleted or no KX records left - expected
+
+ # Invalid KX records should fail and not be created
+ bad_vals = [
+ (KX_BAD_PREF1, A_RECORD),
+ (KX_BAD_PREF2, ZONE)
+ ]
+ for bad_pref, bad_target in bad_vals:
+ result = tasks.add_dns_record(
+ self.master, ZONE, '@', record_type='kx',
+ record_value=[f'{bad_pref} {bad_target}'], raiseonerr=False)
+ assert result.returncode != 0
+ try:
+ self.resolver.resolve(ZONE, 'KX')
+ raise AssertionError(
+ f"Resolving KX for {ZONE} should have raised "
+ "NoAnswer")
+ except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
+ pass # No KX records - expected
+
+ # =========================================================================
+ # Zone Permission Tests
+ # =========================================================================
+
+ def test_zone_permission(self):
+ """Test DNS zone permission add, verify, and remove operations.
+
+ This test covers zone permission management including adding
+ permission, verifying managedby attribute, removing permission,
+ testing duplicate add, testing non-existent zone, and verifying
+ permission cleanup on zone deletion.
+ """
+ tasks.kinit_admin(self.master)
+ # Clean up if zone exists
+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE)
+
+ # Add zone, then permission to manage it
+ tasks.add_dns_zone(
+ self.master, self.MANAGED_ZONE, admin_email=EMAIL)
+ result = tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE)
+ assert result.returncode == 0
+
+ # Verify managedby attribute is set
+ result = tasks.show_dns_zone(
+ self.master, self.MANAGED_ZONE, all_attrs=True)
+ assert 'managedby' in result.stdout_text.lower()
+
+ # Verify permission is added
+ perm_name = f'Manage DNS zone {self.MANAGED_ZONE}'
+ result = tasks.find_permission(self.master, permission=perm_name)
+ assert result.returncode == 0
+
+ # Remove permission to manage zone
+ result = tasks.remove_dns_zone_permission(
+ self.master, self.MANAGED_ZONE)
+ assert result.returncode == 0
+
+ # Verify managedby attribute is not available
+ result = tasks.show_dns_zone(
+ self.master, self.MANAGED_ZONE, all_attrs=True)
+ assert 'managedby' not in result.stdout_text.lower()
+
+ # Verify permission is removed
+ result = tasks.find_permission(
+ self.master, permission=perm_name, raiseonerr=False)
+ assert result.returncode != 0
+
+ # Add zone with permission, delete zone, verify permission deleted
+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1)
+ tasks.add_dns_zone(
+ self.master, self.MANAGED_ZONE1, admin_email=EMAIL)
+ tasks.add_dns_zone_permission(self.master, self.MANAGED_ZONE1)
+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE1)
+ perm_name1 = f'Manage DNS zone {self.MANAGED_ZONE1}'
+ result = tasks.find_permission(
+ self.master, permission=perm_name1, raiseonerr=False)
+ assert result.returncode != 0
+
+ # Add duplicate permission to manage zone
+ tasks.add_dns_zone_permission(
+ self.master, self.MANAGED_ZONE, raiseonerr=False)
+ result = tasks.add_dns_zone_permission(
+ self.master, self.MANAGED_ZONE, raiseonerr=False)
+ assert result.returncode != 0
+ assert 'already exists' in result.stderr_text
+
+ # Add permission to manage non-existent zone
+ result = tasks.add_dns_zone_permission(
+ self.master, self.NONEXISTENT_ZONE, raiseonerr=False)
+ assert result.returncode != 0
+ assert 'DNS zone not found' in result.stderr_text
+
+ # Remove permission to manage zone again (should fail)
+ tasks.remove_dns_zone_permission(
+ self.master, self.MANAGED_ZONE, raiseonerr=False)
+ result = tasks.remove_dns_zone_permission(
+ self.master, self.MANAGED_ZONE, raiseonerr=False)
+ assert result.returncode != 0
+ assert 'permission not found' in result.stderr_text
+
+ # Remove permission for non-existent zone
+ result = tasks.remove_dns_zone_permission(self.master,
+ self.NONEXISTENT_ZONE,
+ raiseonerr=False)
+ assert result.returncode != 0
+ assert 'DNS zone not found' in result.stderr_text
+
+ # Cleanup zones from zone permission tests
+ tasks.del_dns_zone(self.master, self.MANAGED_ZONE)
+
+ # =========================================================================
+ # Persistent Search Tests
+ # =========================================================================
+
+ def test_psearch(self):
+ """Test persistent search and zone serial updates."""
+ tasks.kinit_admin(self.master)
+
+ # Verify psearch is not used when IPA server is installed
+ result = self.master.run_command([
+ 'grep', 'psearch yes', '/etc/named.conf'
+ ], raiseonerr=False)
+ assert result.returncode != 0
+
+ # Create zone with SOA parameters
+ tasks.add_dns_zone(
+ self.master, self.ZONE_PSEARCH, admin_email=EMAIL,
+ refresh=REFRESH, retry=RETRY, expire=EXPIRE,
+ minimum=MINIMUM, ttl=TTL)
+
+ # Verify zone SOA exists
+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA')
+ assert len(ans) > 0
+
+ # Add TXT record and verify
+ tasks.add_dns_record(self.master, self.ZONE_PSEARCH, 'txt',
+ record_type='txt', record_value=[TXT])
+ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT')
+ assert any(TXT in str(r) for r in ans)
+
+ # Update TXT record and verify
+ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt',
+ record_type='txt', old_value=TXT,
+ new_value=NEW_TXT)
+ ans = self.resolver.resolve(f'txt.{self.ZONE_PSEARCH}', 'TXT')
+ assert any(NEW_TXT in str(r) for r in ans)
+
+ # Get old serial
+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA')
+ old_serial = ans[0].serial
+
+ # Update TXT record again
+ tasks.mod_dns_record(self.master, self.ZONE_PSEARCH, 'txt',
+ record_type='txt', old_value=NEW_TXT,
+ new_value=NEWER_TXT)
+
+ # Verify serial increased
+ ans = self.resolver.resolve(self.ZONE_PSEARCH, 'SOA')
+ new_serial = ans[0].serial
+ assert new_serial > old_serial, (
+ f"New serial ({new_serial}) should be higher "
+ f"than old ({old_serial})")
diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py
index 864d5287f8317a5154aec4c792f56deab7ff0120..bff4b40aef6e5adec21c8929719e99669b80cdf0 100644
--- a/ipatests/test_xmlrpc/test_dns_plugin.py
+++ b/ipatests/test_xmlrpc/test_dns_plugin.py
@@ -1088,7 +1088,7 @@ class test_dns(Declarative):
dict(
- desc='Create record %r in zone %r' % (zone1, name1),
+ desc='Create single A record %r in zone %r' % (name1, zone1),
command=('dnsrecord_add', [zone1, name1], {'arecord': arec2}),
expected={
'value': name1_dnsname,
@@ -1132,8 +1132,20 @@ class test_dns(Declarative):
dict(
- desc='Add A record to %r in zone %r' % (name1, zone1),
- command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}),
+ desc='Delete single A record from %r in zone %r' % (name1, zone1),
+ command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}),
+ expected={
+ 'value': [name1_dnsname],
+ 'summary': u'Deleted record "%s"' % name1,
+ 'result': {'failed': []},
+ },
+ ),
+
+
+ dict(
+ desc='Add multiple A records to %r in zone %r' % (name1, zone1),
+ command=('dnsrecord_add', [zone1, name1],
+ {'arecord': [arec2, arec3]}),
expected={
'value': name1_dnsname,
'summary': None,
@@ -1148,14 +1160,29 @@ class test_dns(Declarative):
dict(
- desc='Remove A record from %r in zone %r' % (name1, zone1),
- command=('dnsrecord_del', [zone1, name1], {'arecord': arec2}),
+ desc='Delete multiple A records from %r in zone %r' % (
+ name1, zone1),
+ command=('dnsrecord_del', [zone1, name1],
+ {'arecord': [arec2, arec3]}),
expected={
'value': [name1_dnsname],
+ 'summary': u'Deleted record "%s"' % name1,
+ 'result': {'failed': []},
+ },
+ ),
+
+
+ dict(
+ desc='Re-add A record %r for subsequent tests' % arec3,
+ command=('dnsrecord_add', [zone1, name1], {'arecord': arec3}),
+ expected={
+ 'value': name1_dnsname,
'summary': None,
'result': {
+ 'dn': name1_dn,
'idnsname': [name1_dnsname],
'arecord': [arec3],
+ 'objectclass': objectclasses.dnsrecord,
},
},
),
@@ -1228,6 +1255,50 @@ class test_dns(Declarative):
},
),
+
+ dict(
+ desc='Try to add invalid AAAA record to %r in zone %r' % (
+ name1, zone1),
+ command=('dnsrecord_add', [zone1, name1],
+ {'aaaarecord': u'invalid:ipv6:addr'}),
+ expected=errors.ValidationError(
+ name='ip_address',
+ error=u'invalid IP address format'),
+ ),
+
+
+ dict(
+ desc='Add AAAA record to %r in zone %r using dnsrecord_add' % (
+ name1, zone1),
+ command=('dnsrecord_add', [zone1, name1], {'aaaarecord': aaaarec1}),
+ expected={
+ 'value': name1_dnsname,
+ 'summary': None,
+ 'result': {
+ 'dn': name1_dn,
+ 'idnsname': [name1_dnsname],
+ 'arecord': [arec3],
+ 'aaaarecord': [aaaarec1],
+ 'objectclass': objectclasses.dnsrecord,
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete AAAA record from %r in zone %r using dnsrecord_del' % (
+ name1, zone1),
+ command=('dnsrecord_del', [zone1, name1], {'aaaarecord': aaaarec1}),
+ expected={
+ 'value': [name1_dnsname],
+ 'summary': None,
+ 'result': {
+ 'idnsname': [name1_dnsname],
+ 'arecord': [arec3],
+ },
+ },
+ ),
+
dict(
desc='Try to add invalid MX record to zone %r using dnsrecord_add' % (zone1),
command=('dnsrecord_add', [zone1, u'@'], {'mxrecord': zone1_ns }),
@@ -1350,6 +1421,37 @@ class test_dns(Declarative):
},
),
+
+ dict(
+ desc='Add NAPTR record to zone %r using dnsrecord_add' % (zone1),
+ command=('dnsrecord_add', [zone1, u'_naptr'],
+ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}),
+ expected={
+ 'value': DNSName(u'_naptr'),
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': DN(('idnsname', '_naptr'), zone1_dn),
+ 'idnsname': [DNSName(u'_naptr')],
+ 'naptrrecord': [u'100 10 "U" "E2U+sip" "" _sip._udp'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete NAPTR record from zone %r using dnsrecord_del' % (
+ zone1),
+ command=('dnsrecord_del', [zone1, u'_naptr'],
+ {'naptrrecord': u'100 10 "U" "E2U+sip" "" _sip._udp'}),
+ expected={
+ 'value': [DNSName(u'_naptr')],
+ 'summary': u'Deleted record "%s"' % u'_naptr',
+ 'result': {'failed': []},
+ },
+ ),
+
+
dict(
desc='Try to add CNAME record to %r using dnsrecord_add' % (name1),
command=('dnsrecord_add', [zone1, name1], {'cnamerecord': absnxname}),
@@ -1456,6 +1558,66 @@ class test_dns(Declarative):
'(RFC 2181, section 6.1)'),
),
+
+ dict(
+ desc='Add DNAME record with underscore to zone %r' % (zone1),
+ command=('dnsrecord_add', [zone1, u'bar_underscore'],
+ {'dnamerecord': absnxname}),
+ expected={
+ 'value': DNSName(u'bar_underscore'),
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': DN(('idnsname', 'bar_underscore'), zone1_dn),
+ 'idnsname': [DNSName(u'bar_underscore')],
+ 'dnamerecord': [absnxname],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete DNAME record with underscore from zone %r' % (zone1),
+ command=('dnsrecord_del', [zone1, u'bar_underscore'],
+ {'dnamerecord': absnxname}),
+ expected={
+ 'value': [DNSName(u'bar_underscore')],
+ 'summary': u'Deleted record "%s"' % u'bar_underscore',
+ 'result': {'failed': []},
+ },
+ ),
+
+
+ dict(
+ desc='Add CERT record to zone %r using dnsrecord_add' % (zone1),
+ command=('dnsrecord_add', [zone1, u'_cert'],
+ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}),
+ expected={
+ 'value': DNSName(u'_cert'),
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': DN(('idnsname', '_cert'), zone1_dn),
+ 'idnsname': [DNSName(u'_cert')],
+ 'certrecord': [u'1 1 1 F835EDA21E94B565716F'],
+ },
+ },
+ ),
+
+
+ dict(
+ desc='Delete CERT record from zone %r using dnsrecord_del' % (
+ zone1),
+ command=('dnsrecord_del', [zone1, u'_cert'],
+ {'certrecord': u'1 1 1 F835EDA21E94B565716F'}),
+ expected={
+ 'value': [DNSName(u'_cert')],
+ 'summary': u'Deleted record "%s"' % u'_cert',
+ 'result': {'failed': []},
+ },
+ ),
+
+
dict(
desc='Add NS+DNAME record to %r zone record using dnsrecord_add' % (zone2),
command=('dnsrecord_add', [zone2, u'@'],
@@ -1527,6 +1689,22 @@ class test_dns(Declarative):
),
+ dict(
+ desc='Delete TXT record from %r using dnsrecord_del' % (name1),
+ command=('dnsrecord_del', [zone1, name1],
+ {'txtrecord': u'foo bar'}),
+ expected={
+ 'value': [name1_dnsname],
+ 'summary': None,
+ 'result': {
+ 'idnsname': [name1_dnsname],
+ 'arecord': [arec3],
+ 'kxrecord': [u'1 foo-1'],
+ },
+ },
+ ),
+
+
dict(
desc='Try to add unresolvable absolute NS record to %r using dnsrecord_add' % (name_ns),
command=(
@@ -1879,6 +2057,38 @@ class test_dns(Declarative):
},
),
+
+ dict(
+ desc='Delete PTR record %r from %r using dnsrecord_del' % (
+ revname1, revzone1),
+ command=('dnsrecord_del', [revzone1, revname1],
+ {'ptrrecord': absnxname}),
+ expected={
+ 'value': [revname1_dnsname],
+ 'summary': u'Deleted record "%s"' % revname1,
+ 'result': {'failed': []},
+ },
+ ),
+
+
+ dict(
+ desc='Re-add PTR record %r to %r for subsequent tests' % (
+ revname1, revzone1),
+ command=('dnsrecord_add', [revzone1, revname1],
+ {'ptrrecord': absnxname}),
+ expected={
+ 'value': revname1_dnsname,
+ 'summary': None,
+ 'result': {
+ 'objectclass': objectclasses.dnsrecord,
+ 'dn': revname1_dn,
+ 'idnsname': [revname1_dnsname],
+ 'ptrrecord': [absnxname],
+ },
+ },
+ ),
+
+
dict(
desc='Update global DNS settings',
command=('dnsconfig_mod', [], {'idnsforwarders' : [fwd_ip],}),
@@ -3114,6 +3324,19 @@ class test_dns(Declarative):
),
+ dict(
+ desc='Delete AFSDB record from %r in zone %r' % (
+ dnsafsdbres1, idnzone1),
+ command=('dnsrecord_del', [idnzone1, dnsafsdbres1],
+ {'afsdbrecord': u'0 ' + idnzone1_mname}),
+ expected={
+ 'value': [dnsafsdbres1_dnsname],
+ 'summary': u'Deleted record "%s"' % dnsafsdbres1,
+ 'result': {'failed': []},
+ },
+ ),
+
+
dict(
desc='Add A denormalized record in zone %r' % (idnzone1),
command=('dnsrecord_add', [idnzone1, u'gro\xdf'], {'arecord': u'172.16.0.1'}),
@@ -6427,6 +6650,61 @@ class test_dns_soa(Declarative):
u"A/AAAA record" %
zone6_unresolvable_ns_dnsname,),
),
+
+ dict(
+ desc='Adding a zone - %r - with invalid SOA refresh value' % zone6,
+ command=(
+ 'dnszone_add', [zone6], {
+ 'idnssoarefresh': 12345678901234,
+ }),
+ expected=errors.ValidationError(
+ name='refresh',
+ error=u'can be at most 2147483647'),
+ ),
+
+ dict(
+ desc='Adding a zone - %r - with invalid SOA retry value' % zone6,
+ command=(
+ 'dnszone_add', [zone6], {
+ 'idnssoaretry': 12345678901234,
+ }),
+ expected=errors.ValidationError(
+ name='retry',
+ error=u'can be at most 2147483647'),
+ ),
+
+ dict(
+ desc='Adding a zone - %r - with invalid SOA expire value' % zone6,
+ command=(
+ 'dnszone_add', [zone6], {
+ 'idnssoaexpire': 12345678901234,
+ }),
+ expected=errors.ValidationError(
+ name='expire',
+ error=u'can be at most 2147483647'),
+ ),
+
+ dict(
+ desc='Adding a zone - %r - with invalid SOA minimum value' % zone6,
+ command=(
+ 'dnszone_add', [zone6], {
+ 'idnssoaminimum': 12345678901234,
+ }),
+ expected=errors.ValidationError(
+ name='minimum',
+ error=u'can be at most 2147483647'),
+ ),
+
+ dict(
+ desc='Adding a zone - %r - with invalid TTL value' % zone6,
+ command=(
+ 'dnszone_add', [zone6], {
+ 'dnsttl': 12345678901234,
+ }),
+ expected=errors.ValidationError(
+ name='ttl',
+ error=u'can be at most 2147483647'),
+ ),
]
--
2.53.0