|
| 1 | +# Copyright (c) 2025 Carnegie Mellon University. |
| 2 | +# NO WARRANTY. THIS CARNEGIE MELLON UNIVERSITY AND SOFTWARE |
| 3 | +# ENGINEERING INSTITUTE MATERIAL IS FURNISHED ON AN "AS-IS" BASIS. |
| 4 | +# CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY KIND, |
| 5 | +# EITHER EXPRESSED OR IMPLIED, AS TO ANY MATTER INCLUDING, BUT |
| 6 | +# NOT LIMITED TO, WARRANTY OF FITNESS FOR PURPOSE OR |
| 7 | +# MERCHANTABILITY, EXCLUSIVITY, OR RESULTS OBTAINED FROM USE |
| 8 | +# OF THE MATERIAL. CARNEGIE MELLON UNIVERSITY DOES NOT MAKE |
| 9 | +# ANY WARRANTY OF ANY KIND WITH RESPECT TO FREEDOM FROM |
| 10 | +# PATENT, TRADEMARK, OR COPYRIGHT INFRINGEMENT. |
| 11 | +# Licensed under a MIT (SEI)-style license, please see LICENSE or contact |
| 12 | +# permission@sei.cmu.edu for full terms. |
| 13 | +# [DISTRIBUTION STATEMENT A] This material has been approved for |
| 14 | +# public release and unlimited distribution. Please see Copyright notice |
| 15 | +# for non-US Government use and distribution. |
| 16 | +# This Software includes and/or makes use of Third-Party Software each |
| 17 | +# subject to its own license. |
| 18 | +# DM24-0278 |
| 19 | + |
| 20 | +import logging |
| 21 | +import re |
| 22 | +import unittest |
| 23 | + |
| 24 | +from ssvc.namespaces import ( |
| 25 | + BASE_NS_PATTERN, |
| 26 | + BASE_PATTERN, |
| 27 | + LENGTH_CHECK_PATTERN, |
| 28 | + MAX_NS_LENGTH, |
| 29 | + MIN_NS_LENGTH, |
| 30 | + NS_PATTERN, |
| 31 | +) |
| 32 | + |
| 33 | +logger = logging.getLogger(__name__) |
| 34 | + |
| 35 | + |
| 36 | +class TestNamespacePattern(unittest.TestCase): |
| 37 | + def setUp(self): |
| 38 | + self.expect_success = [ |
| 39 | + "ssvc", |
| 40 | + "cisa", |
| 41 | + "custom", # not in enum, but valid for the pattern |
| 42 | + "x_private-test", # valid namespace with dash |
| 43 | + "x_custom", # valid namespace with x_ prefix |
| 44 | + "x_custom.with.dots", # valid namespace with x_ prefix and dots |
| 45 | + "abc", # not in enum, but valid for the pattern |
| 46 | + "x_abc", # valid namespace with x_ prefix |
| 47 | + "x_custom//extension", # double slash is okay when it's the first segment |
| 48 | + "ssvc/de-DE/reference-arch-1", # valid BCP-47 tag with dashes |
| 49 | + "x_test/pl-PL/foo/bar/baz/quux", # valid BCP-47 tag and multiple segments |
| 50 | + ] |
| 51 | + self.expect_fail = [ |
| 52 | + "999", # invalid namespace, numeric only |
| 53 | + "99xx", # invalid namespace, numeric prefix |
| 54 | + "x__invalid", # invalid namespace, double underscore |
| 55 | + "x_-invalid", # invalid namespace, dash after x_ |
| 56 | + "x_.invalid", # invalid namespace, dash at end |
| 57 | + "x_/foo", # invalid namespace, slash after x_, invalid BCP-47 tag |
| 58 | + "x_//foo", # invalid namespace, double slash after x_ |
| 59 | + "x_abc/invalid-bcp-47", # not a valid BCP-47 tag |
| 60 | + "abc/invalid-bcp-47", # not in enum (but that's ok for the pattern), not a valid BCP-47 tag |
| 61 | + "abc/invalid", # not in enum (but that's ok for the pattern), not a valid BCP-47 tag |
| 62 | + "x_custom/extension", # not a valid BCP-47 tag |
| 63 | + "x_test/not-bcp-47", # not a valid BCP-47 tag |
| 64 | + "x_custom/extension/with/multiple/segments/" |
| 65 | + + "a" * 990, # exceeds max length |
| 66 | + "x_custom.extension.", # ends with punctuation |
| 67 | + "x_custom..extension", # double dot |
| 68 | + "x_custom/", # ends with slash |
| 69 | + "x_custom/extension//", # double slash at end |
| 70 | + "x_custom/extension/with//double/slash", # double slash in middle |
| 71 | + "x_custom/extension/with..double.dot", # double dot in middle |
| 72 | + "x_custom/extension/with--double-dash", # double dash in middle |
| 73 | + "ab", # too short |
| 74 | + "x_", # too short after prefix |
| 75 | + ] |
| 76 | + |
| 77 | + def test_ns_pattern(self): |
| 78 | + |
| 79 | + self._test_successes_failures( |
| 80 | + NS_PATTERN.pattern, self.expect_fail, self.expect_success |
| 81 | + ) |
| 82 | + |
| 83 | + def test_base_pattern(self): |
| 84 | + x_success = [ |
| 85 | + "abc", |
| 86 | + "contains.dot", |
| 87 | + "contains-dash", |
| 88 | + "contains-dash-and.dot", |
| 89 | + ] |
| 90 | + x_fail = [ |
| 91 | + "a", # too short |
| 92 | + "ab", # too short |
| 93 | + "9abc", # starts with a number |
| 94 | + "x_foo", # no x_ in base pattern |
| 95 | + "contains..double.dot", # double dot |
| 96 | + "contains--double-dash", # double dash |
| 97 | + "contains_underscore", # underscore not allowed |
| 98 | + "contains/slash", # slash not allowed |
| 99 | + ".starts.with.dot", # starts with a dot |
| 100 | + "-starts-with-dash", # starts with a dash |
| 101 | + "/starts-with-slash", # starts with a slash |
| 102 | + "_starts-with-underscore", # starts with an underscore |
| 103 | + "ends-with-dot.", # ends with a dot |
| 104 | + "ends-with-dash-", # ends with a dash |
| 105 | + "ends-with-slash/", # ends with a slash |
| 106 | + ] |
| 107 | + self._test_successes_failures(BASE_PATTERN, x_fail, x_success) |
| 108 | + |
| 109 | + def test_experimental_base_pattern(self): |
| 110 | + x_success = [ |
| 111 | + "x_abc", |
| 112 | + "x_custom", |
| 113 | + "x_custom.with.dots", # dots are allowed in the base pattern |
| 114 | + "x_custom-with-dashes", # dashes are allowed in the base pattern |
| 115 | + ] |
| 116 | + x_fail = [ |
| 117 | + "9abc", # does not start with x_ |
| 118 | + "x__invalid", # double underscore |
| 119 | + "x_-invalid", # dash after x_ |
| 120 | + "x_.invalid", # dash at end |
| 121 | + "x_9abc", # starts with a number after x_ |
| 122 | + "x_abc.", # ends with a dot |
| 123 | + "x_abc-", # ends with a dash |
| 124 | + "x_abc/", # ends with a slash |
| 125 | + "x_/foo", # slashes aren't part of the base pattern |
| 126 | + ] |
| 127 | + self._test_successes_failures(BASE_NS_PATTERN, x_fail, x_success) |
| 128 | + |
| 129 | + def test_base_ns_pattern(self): |
| 130 | + x_success = [ |
| 131 | + "abc", |
| 132 | + "x_abc", |
| 133 | + "x_custom", |
| 134 | + "x_custom.with.dots", # dots are allowed in the base pattern |
| 135 | + "x_custom-with-dashes", # dashes are allowed in the base pattern |
| 136 | + ] |
| 137 | + x_fail = [ |
| 138 | + "9abc", # starts with a number |
| 139 | + "x__invalid", # double underscore |
| 140 | + "x_-invalid", # dash after x_ |
| 141 | + "x_.invalid", # dash at end |
| 142 | + "x_9abc", # starts with a number after x_ |
| 143 | + "x_abc.", # ends with a dot |
| 144 | + "x_abc-", # ends with a dash |
| 145 | + "x_abc/", # ends with a slash |
| 146 | + "x_/foo", # slashes aren't part of the base pattern |
| 147 | + ] |
| 148 | + self._test_successes_failures(BASE_NS_PATTERN, x_fail, x_success) |
| 149 | + |
| 150 | + def _test_successes_failures( |
| 151 | + self, pattern: str, x_fail: list[str], x_success: list[str] |
| 152 | + ): |
| 153 | + successes = [] |
| 154 | + failures = [] |
| 155 | + # if pattern is not anchored, anchor it |
| 156 | + if not pattern.startswith("^"): |
| 157 | + pattern = "^" + pattern |
| 158 | + if not pattern.endswith("$"): |
| 159 | + pattern = pattern + "$" |
| 160 | + |
| 161 | + for ns in x_success: |
| 162 | + expected = f"Should match {ns}" |
| 163 | + if re.match(pattern, ns) is None: |
| 164 | + failures.append(expected) |
| 165 | + else: |
| 166 | + successes.append(expected) |
| 167 | + for ns in x_fail: |
| 168 | + expected = f"Should not match {ns}" |
| 169 | + if re.match(pattern, ns) is not None: |
| 170 | + failures.append(expected) |
| 171 | + else: |
| 172 | + successes.append(expected) |
| 173 | + logger.debug(f"Successes: {successes}") |
| 174 | + self.assertFalse(failures) |
| 175 | + |
| 176 | + def test_length_check_pattern(self): |
| 177 | + """ |
| 178 | + Test the length check pattern for namespaces. |
| 179 | + The pattern should enforce a minimum and maximum length. |
| 180 | + """ |
| 181 | + min_length = MIN_NS_LENGTH |
| 182 | + max_length = MAX_NS_LENGTH |
| 183 | + |
| 184 | + valid_ns = "x_valid_namespace" |
| 185 | + too_short_ns = "x_v" |
| 186 | + too_long_ns = "x_" + "a" * (max_length - 2) |
| 187 | + |
| 188 | + for i in range(0, MIN_NS_LENGTH): |
| 189 | + # should fail for lengths less than MIN_NS_LENGTH |
| 190 | + ns = "a" * i |
| 191 | + self.assertIsNone( |
| 192 | + re.match(LENGTH_CHECK_PATTERN, ns), f"Should not match: {ns}" |
| 193 | + ) |
| 194 | + |
| 195 | + |
| 196 | +if __name__ == "__main__": |
| 197 | + unittest.main() |
0 commit comments