From d9e23daa87bdd3a967e7e2468b48608ef471e409 Mon Sep 17 00:00:00 2001 From: Bob Halley Date: Thu, 31 Jul 2025 18:08:43 -0700 Subject: [PATCH] improve test coverage; discover bugs; fix bugs! --- dns/btreezone.py | 27 +++++++--- dns/zone.py | 11 ++-- tests/test_btreezone.py | 115 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 tests/test_btreezone.py diff --git a/dns/btreezone.py b/dns/btreezone.py index a96b27ea..c749b4ad 100644 --- a/dns/btreezone.py +++ b/dns/btreezone.py @@ -151,13 +151,24 @@ class WritableVersion(dns.zone.WritableVersion): else: self.delegations = Delegations() - def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node: - node = super()._maybe_cow(name) - if name == self.zone.origin: - node.flags |= NodeFlags.ORIGIN # type: ignore + def _is_origin(self, name: dns.name.Name) -> bool: + # Assumes name has already been validated (and thus adjusted to the right + # relativity too) + if self.zone.relativize: + return name == dns.name.empty + else: + return name == self.zone.origin + + def _maybe_cow_with_name( + self, name: dns.name.Name + ) -> Tuple[dns.node.Node, dns.name.Name]: + (node, name) = super()._maybe_cow_with_name(name) + node = cast(Node, node) + if self._is_origin(name): + node.flags |= NodeFlags.ORIGIN elif self.delegations.is_glue(name): - node.flags |= NodeFlags.GLUE # type: ignore - return node + node.flags |= NodeFlags.GLUE + return (node, name) def update_glue_flag(self, name: dns.name.Name, is_glue: bool) -> None: cursor = self.nodes.cursor() # type: ignore @@ -203,7 +214,7 @@ class WritableVersion(dns.zone.WritableVersion): def put_rdataset( self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset ) -> None: - node = self._maybe_cow(name) + (node, name) = self._maybe_cow_with_name(name) if ( rdataset.rdtype == dns.rdatatype.NS and not node.is_origin_or_glue() # type: ignore ): @@ -219,7 +230,7 @@ class WritableVersion(dns.zone.WritableVersion): rdtype: dns.rdatatype.RdataType, covers: dns.rdatatype.RdataType, ) -> None: - node = self._maybe_cow(name) + (node, name) = self._maybe_cow_with_name(name) if rdtype == dns.rdatatype.NS and name in self.delegations: # type: ignore node.flags &= ~NodeFlags.DELEGATION # type: ignore self.delegations.discard(name) # type: ignore diff --git a/dns/zone.py b/dns/zone.py index cfb89ec4..05170fe8 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -1028,7 +1028,9 @@ class WritableVersion(Version): self.origin = zone.origin self.changed: Set[dns.name.Name] = set() - def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node: + def _maybe_cow_with_name( + self, name: dns.name.Name + ) -> Tuple[dns.node.Node, dns.name.Name]: name = self._validate_name(name) node = self.nodes.get(name) if node is None or name not in self.changed: @@ -1046,9 +1048,12 @@ class WritableVersion(Version): new_node.rdatasets.extend(node.rdatasets) self.nodes[name] = new_node self.changed.add(name) - return new_node + return (new_node, name) else: - return node + return (node, name) + + def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node: + return self._maybe_cow_with_name(name)[0] def delete_node(self, name: dns.name.Name) -> None: name = self._validate_name(name) diff --git a/tests/test_btreezone.py b/tests/test_btreezone.py new file mode 100644 index 00000000..f2f5e7dc --- /dev/null +++ b/tests/test_btreezone.py @@ -0,0 +1,115 @@ +from typing import cast + +import dns.btreezone +import dns.rdataset +import dns.zone + +Node = dns.btreezone.Node + +simple_zone = """ +$ORIGIN example. +$TTL 300 +@ soa foo bar 1 2 3 4 5 +@ ns ns1 +@ ns ns2 +ns1 a 10.0.0.1 +ns2 a 10.0.0.2 +sub ns ns1.sub +sub ns ns2.sub +ns1.sub a 10.0.0.3 +ns2.sub a 10.0.0.4 +ns1.sub2 a 10.0.0.5 +ns2.sub2 a 10.0.0.6 +text txt "here to be after sub2" +""" + + +def make_example(text: str, relativize: bool = False) -> dns.btreezone.Zone: + z = dns.zone.from_text( + simple_zone, "example.", relativize=relativize, zone_factory=dns.btreezone.Zone + ) + return cast(dns.btreezone.Zone, z) + + +def do_test_node_flags(relativize: bool): + z = make_example(simple_zone, relativize) + n = cast(Node, z.get_node("@")) + assert not n.is_delegation() + assert not n.is_glue() + assert n.is_origin() + assert n.is_origin_or_glue() + assert n.is_immutable() + n = cast(Node, z.get_node("sub")) + assert n.is_delegation() + assert not n.is_glue() + assert not n.is_origin() + assert not n.is_origin_or_glue() + n = cast(Node, z.get_node("ns1.sub")) + assert not n.is_delegation() + assert n.is_glue() + assert not n.is_origin() + assert n.is_origin_or_glue() + + +def test_node_flags_absolute(): + do_test_node_flags(False) + + +def test_node_flags_relative(): + do_test_node_flags(True) + + +def test_flags_in_constructor(): + n = Node() + assert n.flags == 0 + n = Node(dns.btreezone.NodeFlags.ORIGIN) + assert n.is_origin() + + +def do_test_obscure_and_expose(relativize: bool): + z = make_example(simple_zone, relativize=relativize) + n = cast(Node, z.get_node("ns1.sub2")) + assert not n.is_delegation() + assert not n.is_glue() + assert not n.is_origin() + assert not n.is_origin_or_glue() + rds = dns.rdataset.from_text("in", "ns", 300, "ns1.sub2", "ns2.sub2") + with z.writer() as txn: + txn.replace("sub2", rds) + n = cast(Node, z.get_node("ns1.sub2")) + assert not n.is_delegation() + assert n.is_glue() + assert not n.is_origin() + assert n.is_origin_or_glue() + with z.writer() as txn: + txn.delete("sub2") + txn.delete("ns2.sub2") # for other coverage purposes! + n = cast(Node, z.get_node("ns1.sub2")) + assert not n.is_delegation() + assert not n.is_glue() + assert not n.is_origin() + assert not n.is_origin_or_glue() + # repeat but delete just the rdataset + rds = dns.rdataset.from_text("in", "ns", 300, "ns1.sub2", "ns2.sub2") + with z.writer() as txn: + txn.replace("sub2", rds) + n = cast(Node, z.get_node("ns1.sub2")) + assert not n.is_delegation() + assert n.is_glue() + assert not n.is_origin() + assert n.is_origin_or_glue() + with z.writer() as txn: + txn.delete("sub2", "NS") + n = cast(Node, z.get_node("ns1.sub2")) + assert not n.is_delegation() + assert not n.is_glue() + assert not n.is_origin() + assert not n.is_origin_or_glue() + + +def test_obscure_and_expose_absolute(): + do_test_obscure_and_expose(False) + + +def test_obscure_and_expose_relative(): + do_test_obscure_and_expose(True) -- 2.47.3