From a6c57c49a5f7a8e617b16f8c223b2a96f9b64119 Mon Sep 17 00:00:00 2001 From: Pawel Kowalik Date: Wed, 1 Apr 2026 02:16:20 +0200 Subject: [PATCH 1/3] APEXCNEME Template validation error Fixes #40 --- domainconnectzone/DomainConnectImpl.py | 4 ++-- test/test_DomainConnectTemplates.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/domainconnectzone/DomainConnectImpl.py b/domainconnectzone/DomainConnectImpl.py index 9e531cc..7d471f9 100644 --- a/domainconnectzone/DomainConnectImpl.py +++ b/domainconnectzone/DomainConnectImpl.py @@ -1221,7 +1221,7 @@ def get_records_variables(template_records, group=None): if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'NS', 'TXT', 'SPFM', 'REDIR301', 'REDIR302']: get_record_variables(template_record, template_record['host'], params) - if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'NS']: + if template_record_type in ['A', 'AAAA', 'MX', 'CNAME', 'NS', 'APEXCNAME']: get_record_variables(template_record, template_record['pointsTo'], params) if template_record_type in ['TXT']: @@ -1244,7 +1244,7 @@ def get_records_variables(template_records, group=None): if template_record_type in ['REDIR301', 'REDIR302']: get_record_variables(template_record, template_record['target'], params) - if (template_record_type not in ['A', 'AAAA', 'MX', 'CNAME', 'TXT', 'SRV', + if (template_record_type not in ['A', 'AAAA', 'MX', 'CNAME', 'TXT', 'SRV', 'APEXCNAME', 'SPFM', 'NS', 'REDIR301', 'REDIR302'] and is_custom_record_type(template_record_type)): get_record_variables(template_record, template_record['host'], params) diff --git a/test/test_DomainConnectTemplates.py b/test/test_DomainConnectTemplates.py index db020a2..0b2f40b 100644 --- a/test/test_DomainConnectTemplates.py +++ b/test/test_DomainConnectTemplates.py @@ -238,6 +238,18 @@ def test_schema_validation_error(self): with self.assertRaises(InvalidTemplate): self._dct.validate_template(invalid_template) + def test_validate_template_apexcname_record(self): + # validate_template must succeed for a template containing an APEXCNAME record. + # Regression: APEXCNAME was falling into the custom-type branch which accessed + # template_record['data'], but APEXCNAME uses 'pointsTo' — causing a KeyError + # wrapped as "Template validation error: 'data'". + import copy + template = copy.deepcopy(self.template_base) + template["records"] = [ + {"type": "APEXCNAME", "host": "@", "pointsTo": "foo.com", "ttl": 3600} + ] + self._dct.validate_template(template) # must not raise + class TestDomainConnectTemplatesVariableNames(unittest.TestCase): @@ -265,6 +277,14 @@ def test_get_variable_names_with_variables(self): result = DomainConnectTemplates.get_variable_names(self.template, variables=variables) self.assertEqual(result, {"domain": '', "host": '', "group": '', "d": "customdomain.com", "h": None}) + def test_get_variable_names_pointsto_variable_in_apexcname_record(self): + # Variables in pointsTo of APEXCNAME must be extracted + template = {"records": [ + {"type": "APEXCNAME", "host": "@", "pointsTo": "%target%", "ttl": 300}, + ]} + result = DomainConnectTemplates.get_variable_names(template) + self.assertIn("target", result) + def test_get_variable_names_ttl_variable_in_a_record(self): # %variable% in ttl of an A record must be detected template = {"records": [ From 99b78b486660ee955ccc4c4f0203a4bb53bee14d Mon Sep 17 00:00:00 2001 From: Pawel Kowalik Date: Wed, 1 Apr 2026 02:21:12 +0200 Subject: [PATCH 2/3] Update AI documentation --- CLAUDE.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5897c33..0e944a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ This is a Python library (`domainconnectzone`) implementing the [Domain Connect ### Core concepts -- **Zone records**: Dicts with `type`, `name`, `data`, `ttl`, and optional `_dc` (template provenance metadata), `_delete`, `_replace` flags. Types: A, AAAA, CNAME, NS, TXT, MX, SRV. +- **Zone records**: Dicts with `type`, `name`, `data`, `ttl`, and optional `_dc` (template provenance metadata), `_delete`, `_replace` flags. Types: A, AAAA, CNAME, APEXCNAME, NS, TXT, MX, SRV. - **Templates**: JSON files named `{providerId}.{serviceId}.json` stored in `domainconnectzone/templates/`. They contain record definitions with `%variable%` substitution syntax. - **Host resolution**: Template record names are relative to the applied host/domain. `@` means the apex. Variables `%domain%`, `%host%`, `%fqdn%` are always available. @@ -79,7 +79,9 @@ Tests live in `test/` and use `unittest`. Test templates are in `test/test_defin | `test_qsutils.py` | Query-string utility module | | `test_process_records.py` | YAML-driven harness — scans for `suite_type: process_records` files | | `test_apply_template.py` | YAML-driven harness — scans for `suite_type: apply_template` files | -| `test_definitions/process_records_tests.yaml` | 88 language-agnostic process_records compliance tests | +| `test_definitions/process_records_tests.yaml` | 265 language-agnostic process_records compliance tests | +| `test_definitions/process_records_apexcname_tests.yaml` | 7 process_records tests for APEXCNAME | +| `test_definitions/process_records_redir_tests.yaml` | 7 process_records tests for REDIR301/REDIR302 | | `test_definitions/apply_template_tests.yaml` | 12 language-agnostic apply_template compliance tests | | `harness_utils.py` | Shared helpers for the two YAML harnesses (not a test module) | @@ -92,13 +94,15 @@ The two YAML harnesses scan the test directory for `*.yaml` files matching their When applying a template, existing zone records are marked `_delete=1` based on type-specific rules: - **A/AAAA**: deletes same-host records of same type - **CNAME**: deletes all non-NS records at that host +- **APEXCNAME**: always targets apex (`@`); deletes A, AAAA, CNAME, MX, TXT, REDIR301, REDIR302 at `@`; uses `pointsTo` field (not `data`) in template records - **TXT**: deletes based on `txtConflictMatchingMode` (None/All/Prefix) -- **MX/SRV/NS**: deletes same-host records of same type +- **MX/SRV**: deletes same-host records of same type - **NS**: conflicts with all records of same host or hosts below the delegation point (zone cut) - **Custom/unknown types** (CAA, TYPE256, etc.): no conflict deletion — record is simply added; `data` supports `%variable%` substitution and `@`/empty resolves to fqdn ### Development rules - if asked to check or develop test, look only in the test code and test definition files. NEVER analyse the code to see how the code bahaves. It is ok or even desired for test code to fail after adding or modifying a test (test driven development). - if a newly written or modified test fails unexpectedly, do NOT silently correct it — stop and ask the user whether the failure indicates an error in the test itself (wrong expectation) or a real implementation bug that should be fixed. Only correct the test after the user confirms it is wrong. -- when asked to add a test of a certain pattern add a test for each RR type, incl. custom RR type, unless explicitly asked to only consider one RR type. The full set of RR types is: A, AAAA, CNAME, TXT, MX, NS, SRV, and at least one custom type (e.g. CAA). Note that SRV uses `name` (not `host`) in template_records, and some patterns (e.g. empty name) may be invalid for SRV due to protocol constraints — use `InvalidData` as the expected outcome in those cases rather than skipping the type. -- develop code only when asked \ No newline at end of file +- when asked to add a test of a certain pattern add a test for each RR type, incl. custom RR type, unless explicitly asked to only consider one RR type. The full set of RR types is: A, AAAA, CNAME, APEXCNAME, TXT, MX, NS, SRV, and at least one custom type (e.g. CAA). Note that SRV uses `name` (not `host`) in template_records, and some patterns (e.g. empty name) may be invalid for SRV due to protocol constraints — use `InvalidData` as the expected outcome in those cases rather than skipping the type. +- develop code only when asked +- when adding support for a new RR type, update **both** `process_records` (the processing/conflict logic in `DomainConnectImpl.py`) **and** `get_records_variables` (the variable extraction function in the same file). Omitting either causes runtime errors or silent variable extraction failures for that type. \ No newline at end of file From f458a07093647363b75ba89e254b5f6379d4e044 Mon Sep 17 00:00:00 2001 From: Pawel Kowalik Date: Wed, 1 Apr 2026 02:21:45 +0200 Subject: [PATCH 3/3] Version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7dcc7f9..173d5e0 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def run(self): setup( name='domainconnectzone', - version='4.7.1', + version='4.7.2', description=DESCRIPTION, author='domainconnect.org', url='https://github.com/Domain-Connect/domainconnectzone',