Skip to content

Commit e41b5bc

Browse files
seanthegeekclaude
andauthored
Release 5.16.2: BIMI x/y attribute fix; narrower dnssec exceptions (#254)
* Remove coverage-chasing tests; fix BIMI x/y attribute check Drop tests that asserted on contrived inputs solely to keep coverage high, and fix the underlying bugs they were papering over. - checkdmarc/bimi.py: SVG x/y forbidden-attribute capture was reading the wrong xmltodict keys ("x"/"y" matches child elements, not attributes), so the check_svg_requirements rejection never fired on real SVGs. Switch to "@x"/"@y" and fix a typo that wrote the y value into metadata["x"]. - checkdmarc/dnssec.py: Narrow three broad "except Exception" clauses to (dns.exception.DNSException, OSError, EOFError) per AGENTS.md. - tests/test_bimi.py: Delete TestSvgMetadataExtraAttributes; it built malformed SVGs (with child <x>/<y>/<description>/<overflow> elements) to exercise unreachable branches and even asserted on a known source bug. Replace with one honest end-to-end test that proves the fixed forbidden-attribute path works. - tests/test_init.py: Delete testKnownGoodMocked — it patched all nine check_* helpers to return canned valid dicts then asserted the canned values came back. The network counterpart testKnownGood is the real test. - tests/test_dnssec.py: Delete testValidationExceptionContinues — it existed only to exercise the broad except we've now narrowed away. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Apply ruff format to tests/test_dnssec.py Trailing blank line at end of file. The previous commit ran ruff check but missed ruff format. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Bump version to 5.16.2 and update changelog Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cover the narrowed DNSSEC except clause with a real ValidationFailure The narrowed exception handler in test_dnssec catches the actual exception types dnspython raises. Exercise it with a real dns.dnssec.ValidationFailure (the exception thrown on a genuine bad signature) and assert on the function's contract: invalid DNSSEC reports as not validated, rather than propagating. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Import ValidationFailure from dns.exception dns.dnssec re-imports it from dns.exception but doesn't re-export it, so pyright flags the dns.dnssec.ValidationFailure reference as a reportPrivateImportUsage error. Use the canonical path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Sean Whalen <seanthegeek@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0fb62ac commit e41b5bc

7 files changed

Lines changed: 37 additions & 112 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 5.16.2
4+
5+
### Changes
6+
7+
- BIMI: forbidden `x`/`y` attributes on the root `<svg>` element are now
8+
actually rejected. `get_svg_metadata` was reading the wrong xmltodict
9+
keys, so the existing rejection in `check_svg_requirements` never fired
10+
on real SVGs. The metadata also lost the `y` value to a typo that
11+
clobbered `metadata["x"]`.
12+
- DNSSEC: narrowed three broad `except Exception` clauses to specific
13+
exception types (`dns.exception.DNSException`, `OSError`, `EOFError`)
14+
so programming errors propagate instead of being silently swallowed.
15+
316
## 5.16.1
417

518
### Changes

checkdmarc/_constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
See the License for the specific language governing permissions and
2020
limitations under the License."""
2121

22-
__version__ = "5.16.1"
22+
__version__ = "5.16.2"
2323

2424
OS = platform.system()
2525
OS_RELEASE = platform.release()

checkdmarc/bimi.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,10 @@ def get_svg_metadata(raw_xml: Union[str, bytes]) -> dict[str, Any]:
503503
view_box = view_box.split(" ")
504504
width = float(view_box[-2])
505505
height = float(view_box[-1])
506-
if "x" in svg.keys():
507-
metadata["x"] = svg["x"]
508-
if "y" in svg.keys():
509-
metadata["x"] = svg["y"]
506+
if "@x" in svg.keys():
507+
metadata["x"] = svg["@x"]
508+
if "@y" in svg.keys():
509+
metadata["y"] = svg["@y"]
510510
if "title" in svg.keys():
511511
metadata["title"] = svg["title"]
512512
description = None

checkdmarc/dnssec.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections.abc import Sequence
99

1010
import dns.dnssec
11+
import dns.exception
1112
import dns.message
1213
import dns.query
1314
import dns.resolver
@@ -105,7 +106,7 @@ def get_dnskey(
105106
key = {name: rrset}
106107
cache[domain] = key
107108
return key
108-
except Exception as e:
109+
except (dns.exception.DNSException, OSError, EOFError) as e:
109110
cache[domain] = None
110111
logging.debug(f"DNSKEY query error: {e}")
111112

@@ -169,7 +170,7 @@ def test_dnssec(
169170
logging.debug(f"Found a signed {rdatatype.name} record")
170171
cache[domain] = True
171172
return True
172-
except Exception as e:
173+
except (dns.exception.DNSException, OSError, EOFError) as e:
173174
logging.debug(f"DNSSEC query error: {e}")
174175

175176
cache[domain] = False
@@ -246,7 +247,7 @@ def get_tlsa_records(
246247
tlsa_records = list(map(lambda x: str(x), list(rrset.items.keys())))
247248
cache[query_hostname] = tlsa_records
248249
return tlsa_records
249-
except Exception as e:
250+
except (dns.exception.DNSException, OSError, EOFError) as e:
250251
logging.debug(f"TLSA query error: {e}")
251252
return tlsa_records
252253
return tlsa_records

tests/test_bimi.py

Lines changed: 10 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -463,66 +463,24 @@ def testRecordNotFoundError(self):
463463
self.assertIn("error", cast(Any, result))
464464

465465

466-
class TestSvgMetadataExtraAttributes(unittest.TestCase):
467-
"""Cover the optional-attribute branches of get_svg_metadata.
466+
class TestSvgMetadataForbiddenAttributes(unittest.TestCase):
467+
"""SVG x/y attributes on the root <svg> are forbidden by BIMI; they
468+
should be captured in metadata so check_svg_requirements can flag them."""
468469

469-
Note: the source checks ``"x" in svg.keys()`` (etc.) — xmltodict puts
470-
attributes under ``@x``, so these branches only fire when the SVG has
471-
*child elements* literally named x / y / overflow. That's contrived
472-
but the tests below construct such SVGs to exercise the branches.
473-
"""
474-
475-
def testXChildElementPopulatesMetadata(self):
476-
svg = (
477-
'<?xml version="1.0" encoding="UTF-8"?>'
478-
'<svg xmlns="http://www.w3.org/2000/svg" version="1.2" '
479-
'baseProfile="tiny-ps" viewBox="0 0 64 64">'
480-
"<x>10</x>"
481-
"<title>Brand</title>"
482-
"</svg>"
483-
)
484-
metadata = checkdmarc.bimi.get_svg_metadata(svg)
485-
self.assertEqual(metadata["x"], "10")
486-
487-
def testYChildElementPopulatesMetadata(self):
488-
"""bimi.py:509 writes the y value to metadata["x"] (source bug);
489-
we exercise the branch only."""
490-
svg = (
491-
'<?xml version="1.0" encoding="UTF-8"?>'
492-
'<svg xmlns="http://www.w3.org/2000/svg" version="1.2" '
493-
'baseProfile="tiny-ps" viewBox="0 0 64 64">'
494-
"<y>5</y>"
495-
"<title>Brand</title>"
496-
"</svg>"
497-
)
498-
metadata = checkdmarc.bimi.get_svg_metadata(svg)
499-
# The branch wrote to metadata["x"] (not metadata["y"]) per the
500-
# source bug — the goal here is line coverage, not correctness.
501-
self.assertEqual(metadata["x"], "5")
502-
503-
def testDescriptionChildElement(self):
470+
def testRootXYAttributesCaptured(self):
504471
svg = (
505472
'<?xml version="1.0" encoding="UTF-8"?>'
506473
'<svg xmlns="http://www.w3.org/2000/svg" version="1.2" '
507-
'baseProfile="tiny-ps" viewBox="0 0 64 64">'
508-
"<title>Brand</title>"
509-
"<description>A brand logo</description>"
510-
"</svg>"
511-
)
512-
metadata = checkdmarc.bimi.get_svg_metadata(svg)
513-
self.assertEqual(metadata["description"], "A brand logo")
514-
515-
def testOverflowChildElement(self):
516-
svg = (
517-
'<?xml version="1.0" encoding="UTF-8"?>'
518-
'<svg xmlns="http://www.w3.org/2000/svg" version="1.2" '
519-
'baseProfile="tiny-ps" viewBox="0 0 64 64">'
520-
"<overflow>hidden</overflow>"
474+
'baseProfile="tiny-ps" viewBox="0 0 64 64" x="0" y="0">'
521475
"<title>Brand</title>"
522476
"</svg>"
523477
)
524478
metadata = checkdmarc.bimi.get_svg_metadata(svg)
525-
self.assertEqual(metadata["overflow"], "hidden")
479+
self.assertEqual(metadata["x"], "0")
480+
self.assertEqual(metadata["y"], "0")
481+
errors = checkdmarc.bimi.check_svg_requirements(metadata)
482+
self.assertTrue(any("cannot include x" in e for e in errors))
483+
self.assertTrue(any("cannot include y" in e for e in errors))
526484

527485

528486
class TestExtractLogoFromPemBytes(unittest.TestCase):

tests/test_dnssec.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,8 +173,10 @@ def testNoSignedRecordsReturnsFalse(self):
173173
)
174174
self.assertFalse(result)
175175

176-
def testValidationExceptionContinues(self):
177-
"""A failure on dns.dnssec.validate is swallowed and we fall through to False"""
176+
def testInvalidSignatureReturnsFalse(self):
177+
"""A bad signature (ValidationFailure) at every rdatatype reports
178+
DNSSEC as not validated, rather than propagating the exception."""
179+
import dns.exception
178180
import dns.rdatatype
179181

180182
rrset = MagicMock()
@@ -188,7 +190,7 @@ def testValidationExceptionContinues(self):
188190
with patch("dns.query.tcp", return_value=response):
189191
with patch(
190192
"dns.dnssec.validate",
191-
side_effect=Exception("invalid signature"),
193+
side_effect=dns.exception.ValidationFailure("bad signature"),
192194
):
193195
result = checkdmarc.dnssec.test_dnssec(
194196
"example.com",

tests/test_init.py

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616
network_test = unittest.skipIf(
1717
OFFLINE_MODE, "Real-network test skipped on GitHub Actions"
1818
)
19-
mocked_only = unittest.skipUnless(
20-
OFFLINE_MODE, "Mocked counterpart skipped locally; network test covers this"
21-
)
2219

2320

2421
class Test(unittest.TestCase):
@@ -53,52 +50,6 @@ def testKnownGood(self):
5350
),
5451
)
5552

56-
@mocked_only
57-
def testKnownGoodMocked(self):
58-
"""check_domains orchestrates per-check helpers and returns valid=True (mocked)
59-
60-
The component check_* helpers each have their own focused tests; this
61-
covers the orchestration in check_domains for the multi-domain code path.
62-
"""
63-
from contextlib import ExitStack
64-
65-
check_returns = {
66-
"checkdmarc.test_dnssec": False,
67-
"checkdmarc.check_soa": {"valid": True, "values": {}},
68-
"checkdmarc.check_ns": {
69-
"hostnames": ["ns1.example.com"],
70-
"warnings": [],
71-
},
72-
"checkdmarc.check_mta_sts": {"valid": False, "error": "not found"},
73-
"checkdmarc.check_mx": {"hosts": [], "warnings": []},
74-
"checkdmarc.check_spf": {
75-
"record": "v=spf1 -all",
76-
"valid": True,
77-
"warnings": [],
78-
},
79-
"checkdmarc.check_dmarc": {
80-
"record": "v=DMARC1; p=reject",
81-
"valid": True,
82-
"warnings": [],
83-
"tags": {},
84-
},
85-
"checkdmarc.check_smtp_tls_reporting": {
86-
"valid": False,
87-
"error": "not found",
88-
},
89-
"checkdmarc.check_bimi": {"valid": True, "warnings": []},
90-
}
91-
with ExitStack() as stack:
92-
for target, return_value in check_returns.items():
93-
stack.enter_context(patch(target, return_value=return_value))
94-
results = checkdmarc.check_domains(known_good_domains)
95-
96-
if not isinstance(results, list):
97-
results = [results]
98-
for result in results:
99-
self.assertTrue(cast(Any, result["spf"])["valid"])
100-
self.assertTrue(result["dmarc"]["valid"])
101-
10253
def testResultsToJson(self):
10354
"""results_to_json produces valid JSON"""
10455
results = cast(

0 commit comments

Comments
 (0)