diff --git a/bin/tests/system/selftest/analyzer.db b/bin/tests/system/selftest/analyzer.db deleted file mode 100644 index 298de3a7bc..0000000000 --- a/bin/tests/system/selftest/analyzer.db +++ /dev/null @@ -1,46 +0,0 @@ -; Copyright (C) Internet Systems Consortium, Inc. ("ISC") -; -; SPDX-License-Identifier: MPL-2.0 -; -; This Source Code Form is subject to the terms of the Mozilla Public -; License, v. 2.0. If a copy of the MPL was not distributed with this -; file, you can obtain one at https://mozilla.org/MPL/2.0/. -; -; See the COPYRIGHT file distributed with this work for additional -; information regarding copyright ownership. - -$TTL 300 ; 5 minutes -@ IN SOA mname1. . ( - 2000042407 ; serial - 20 ; refresh (20 seconds) - 20 ; retry (20 seconds) - 1814400 ; expire (3 weeks) - 3600 ; minimum (1 hour) - ) - NS ns -ns A 10.53.0.3 - -a A 10.0.0.1 -b A 10.0.0.2 -d A 10.0.0.4 -z A 10.0.0.26 -a.a.a.a A 10.0.0.3 -*.wild A 10.0.0.6 -underwild.wild TXT "not occluded" -insecure NS ns.insecure -secondns.insecure NS ns.insecure -insecure NS ns.insecure2 -belowcut.insecure TXT "occluded" -belowcut2.insecure TXT "occluded" ; two RRs, just in case -dname.insecure DNAME occluded. -ns.insecure A 10.53.0.3 -02HC3EM7BDD011A0GMS3HKKJT2IF5VP8 A 10.0.0.17 -dname DNAME elsewhere. -*.dname TXT "occluded" -occluded.dname TXT "occluded" -occluded2.dname TXT "occluded" -nsunder.occluded.dname NS ns -nsunder.dname NS ns -*.nsunder.dname TXT "occluded" - -test. TXT "out of zone" diff --git a/bin/tests/system/selftest/tests_zone_analyzer.py b/bin/tests/system/selftest/tests_zone_analyzer.py index 1808cd4beb..9115d3ca64 100755 --- a/bin/tests/system/selftest/tests_zone_analyzer.py +++ b/bin/tests/system/selftest/tests_zone_analyzer.py @@ -1,5 +1,4 @@ #!/usr/bin/python3 - # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -10,125 +9,203 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +""" +isctest.name.ZoneAnalyzer self-test +Generate insane test zone and check expected output of ZoneAnalyzer utility class +""" -import os + +import collections +import itertools from pathlib import Path +import dns.name +from dns.name import Name import pytest -pytest.importorskip("dns", minversion="2.5.0") -import dns.name - import isctest +import isctest.name -from typing import List - - -SUFFIX = dns.name.from_text("nsec3.example.") -ZONE = isctest.name.ZoneAnalyzer.read_path( - Path(os.environ["builddir"]) / "selftest/analyzer.db", origin=SUFFIX +# set of properies present in the tested zone - read by tests_zone_analyzer.py +CATEGORIES = frozenset( + [ + "delegations", + "dnames", + "ents", + "occluded", + "reachable", + "reachable_delegations", + "reachable_dnames", + "reachable_wildcards", + "wildcards", + ] ) -def text_to_names(texts: List[str]): - return frozenset(dns.name.from_text(text, origin=SUFFIX) for text in texts) +pytestmark = pytest.mark.extra_artifacts(["analyzer.db"]) +SUFFIX = dns.name.from_text("nsec3.example.") + +LABELS = (b"*", b"dname", b"ent", b"ns", b"txt") +LABEL2RRTYPE = { # leftmost label encodes RR type we will synthesize for given name + b"*": "TXT", + b"dname": "DNAME", + b"ent": None, # ENT is not really a 'type' + b"ns": "NS", + b"txt": "TXT", +} +LABEL2TAGS = { # leftmost label encodes 'initial' meaning of a complete name + b"*": {"wildcards"}, + b"dname": {"dnames"}, + b"ns": {"delegations"}, + b"txt": set(), # perhaps reachable, perhaps not, we need to decide based on other labels +} -def test_analyzer_delegations(): - assert ZONE.delegations == text_to_names( - [ - "nsunder.dname", - "nsunder.occluded.dname", - "insecure", - "secondns.insecure", - ] +def name2tags(name): + """ + Decode meaning hidden in labels and their relationships + and return all tags expected from ZoneAnalyzer + """ + tags = LABEL2TAGS[name[0]].copy() + + parent_labels = name[1:] + if b"ns" in parent_labels or b"dname" in parent_labels: + tags.add("occluded") + + if "occluded" not in tags: + if "delegations" in tags: + # delegations are ambiguous and don't count as 'reachable' + tags.add("reachable_delegations") + elif "dnames" in tags: + tags.add("reachable") + tags.add("reachable_dnames") + elif "wildcards" in tags: + tags.add("reachable") + tags.add("reachable_wildcards") + else: + tags.add("reachable") + + return tags + + +def gen_node(nodes, labels): + name = Name(labels) + nodes[name] = name2tags(name) + + +def add_ents(nodes): + """ + Non-occluded nodes with 'ent' as a parent label imply existence of 'ent' nodes. + """ + new_ents = {} + for name, tags in nodes.items(): + if "occluded" in tags: + continue + + # check if any parent is ENT + entidx = 1 + while True: + try: + entidx = name.labels.index(b"ent", entidx) + except ValueError: + break + entname = Name(name[entidx:]) + new_ents[entname] = {"ents"} + entidx += 1 + + return new_ents + + +def is_non_ent(labels): + """ + Filter out nodes with 'ent' at leftmost position. To become ENT a name must + not have data by itself but have some other node defined underneath it, + and must not be occluded, which is something itertools.product() cannot + decide. + """ + return labels[0] != b"ent" + + +def gen_zone(nodes): + """ + Generate zone file in text format. + + All names are relative. + Right-hand side of RRs contains dot-separated list of categories a node + belongs to (except for zone origin). + """ + for name, tags in sorted(nodes.items()): + if len(name) == 0: + # origin, very special case + yield "@\tSOA\treachable. origin-special-case. 0 0 0 0 0\n" + yield "@\tNS\treachable.\n" + yield "@\tA\t192.0.2.1\n" + continue + + rrtype = LABEL2RRTYPE[name[0]] + if rrtype is None: # ENT + prefix = "; " + else: + prefix = "" + assert tags + yield f"{prefix}{name}\t{rrtype}\t{'.'.join(sorted(tags))}.\n" + + +def gen_expected_output(nodes): + """ + {category: set(names)} mapping used by the pytest check + """ + categories = collections.defaultdict(set) + for name, tags in nodes.items(): + for tag in tags: + categories[tag].add(name) + + assert set(categories.keys()) == CATEGORIES, ( + "CATEGORIES needs updating", + CATEGORIES.symmetric_difference(set(categories.keys())), ) - -def test_analyzer_dnames(): - assert ZONE.dnames == text_to_names( - [ - "dname", - "dname.insecure", - ] - ) + return categories -def test_analyzer_ents(): - assert ZONE.ents == text_to_names( - [ - "a.a", - "a.a.a", - "wild", - ] - ) +def generate_test_data(): + """ + Prepare the analyzer.db zone file in the current working directory and + return the expected attribute values for the ZoneAnalyzer instance that + will be tested using that file. + """ + nodes = {} + + for length in range(1, len(LABELS) + 1): + for labelseq in filter(is_non_ent, itertools.product(LABELS, repeat=length)): + gen_node(nodes, labelseq) + + nodes.update(add_ents(nodes)) + + # special-case to make this look as a valid DNS zone - it needs zone origin node + nodes[Name([])] = {"reachable"} + + with open("analyzer.db", "w", encoding="ascii") as outf: + outf.writelines(gen_zone(nodes)) + + return gen_expected_output(nodes) -def test_analyzer_occluded(): - assert ZONE.occluded == text_to_names( - [ - "*.dname", - "nsunder.dname", - "*.nsunder.dname", - "occluded.dname", - "nsunder.occluded.dname", - "occluded2.dname", - "belowcut.insecure", - "belowcut2.insecure", - "dname.insecure", - "ns.insecure", - "secondns.insecure", - ] - ) +if __name__ == "__main__": + generate_test_data() -def test_analyzer_reachable(): - assert ZONE.reachable == text_to_names( - [ - "@", - "02HC3EM7BDD011A0GMS3HKKJT2IF5VP8", - "a", - "a.a.a.a", - "b", - "d", - "dname", - "ns", - "*.wild", - "underwild.wild", - "z", - ] - ) +@pytest.fixture(scope="module") +def analyzer_fixture(): + expected_results = generate_test_data() # creates the "analyzer.db" file + analyzer = isctest.name.ZoneAnalyzer.read_path(Path("analyzer.db"), origin=SUFFIX) + return expected_results, analyzer -def test_analyzer_reachable_delegations(): - assert ZONE.reachable_delegations == text_to_names( - [ - "insecure", - ] - ) - - -def test_analyzer_reachable_dnames(): - assert ZONE.reachable_dnames == text_to_names( - [ - "dname", - ] - ) - - -def test_analyzer_reachable_wildcards(): - assert ZONE.reachable_wildcards == text_to_names( - [ - "*.wild", - ] - ) - - -def test_analyzer_wildcards(): - assert ZONE.wildcards == text_to_names( - [ - "*.dname", - "*.nsunder.dname", - "*.wild", - ] - ) +# pylint: disable=redefined-outer-name +@pytest.mark.parametrize("category", sorted(CATEGORIES)) +def test_analyzer_attrs(category, analyzer_fixture): + expected_results, analyzer = analyzer_fixture + # relativize results to zone name to make debugging easier + results = {name.relativize(SUFFIX) for name in getattr(analyzer, category)} + assert results == expected_results[category]