"""Property-basierte Tests für den StructureManager. **Validates: Requirements 1.3** Property 1: Namensvalidierung akzeptiert nur gültiges kebab-case - For any String, die validate_name-Funktion soll genau dann true zurückgeben, wenn der String ausschließlich aus Kleinbuchstaben, Ziffern und Bindestrichen besteht, zwischen 2 und 50 Zeichen lang ist und nicht mit einem Bindestrich beginnt oder endet. """ from __future__ import annotations import string from pathlib import Path from hypothesis import given, assume, settings from hypothesis import strategies as st from monorepo.structure import StructureManager # --- Strategies --- # Valid kebab-case characters VALID_CHARS = string.ascii_lowercase + string.digits + "-" VALID_START_END_CHARS = string.ascii_lowercase + string.digits @st.composite def valid_kebab_names(draw: st.DrawFn) -> str: """Generates valid kebab-case names: 2-50 chars, [a-z0-9-], no leading/trailing dash.""" length = draw(st.integers(min_value=2, max_value=50)) # First and last char: no dash allowed first = draw(st.sampled_from(VALID_START_END_CHARS)) if length == 2: last = draw(st.sampled_from(VALID_START_END_CHARS)) return first + last last = draw(st.sampled_from(VALID_START_END_CHARS)) # Middle chars can include dashes middle = draw(st.text(alphabet=VALID_CHARS, min_size=length - 2, max_size=length - 2)) return first + middle + last @st.composite def invalid_names_wrong_chars(draw: st.DrawFn) -> str: """Generates names containing at least one invalid character (not [a-z0-9-]).""" # Generate a base valid-ish name, then inject an invalid char invalid_char = draw( st.sampled_from( list(set(string.printable) - set(VALID_CHARS) - set(string.whitespace)) + ["Ä", "ö", "ü", "€", " ", "_", ".", "A", "Z"] ) ) # Place the invalid char in a string of valid length length = draw(st.integers(min_value=2, max_value=50)) position = draw(st.integers(min_value=0, max_value=length - 1)) chars = [] for i in range(length): if i == position: chars.append(invalid_char) else: chars.append(draw(st.sampled_from(VALID_START_END_CHARS))) return "".join(chars) @st.composite def invalid_names_too_short(draw: st.DrawFn) -> str: """Generates names that are too short (0 or 1 character).""" length = draw(st.integers(min_value=0, max_value=1)) return draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length, max_size=length)) @st.composite def invalid_names_too_long(draw: st.DrawFn) -> str: """Generates names that are too long (>50 characters).""" length = draw(st.integers(min_value=51, max_value=100)) first = draw(st.sampled_from(VALID_START_END_CHARS)) last = draw(st.sampled_from(VALID_START_END_CHARS)) middle = draw(st.text(alphabet=VALID_CHARS, min_size=length - 2, max_size=length - 2)) return first + middle + last @st.composite def invalid_names_leading_dash(draw: st.DrawFn) -> str: """Generates names that start with a dash.""" length = draw(st.integers(min_value=2, max_value=50)) rest = draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length - 1, max_size=length - 1)) return "-" + rest @st.composite def invalid_names_trailing_dash(draw: st.DrawFn) -> str: """Generates names that end with a dash.""" length = draw(st.integers(min_value=2, max_value=50)) rest = draw(st.text(alphabet=VALID_START_END_CHARS, min_size=length - 1, max_size=length - 1)) return rest + "-" # --- Tests --- manager = StructureManager(root_path=Path("/tmp/test-monorepo")) class TestProperty1NameValidation: """Property 1: Namensvalidierung akzeptiert nur gültiges kebab-case. **Validates: Requirements 1.3** """ @given(name=valid_kebab_names()) @settings(max_examples=200) def test_valid_names_are_accepted(self, name: str) -> None: """Valid kebab-case names (2-50 chars, [a-z0-9-], no leading/trailing dash) must be accepted by validate_name.""" assert manager.validate_name(name) is True, ( f"validate_name rejected valid name: '{name}'" ) @given(name=invalid_names_wrong_chars()) @settings(max_examples=200) def test_names_with_invalid_chars_are_rejected(self, name: str) -> None: """Names containing characters outside [a-z0-9-] must be rejected.""" assert manager.validate_name(name) is False, ( f"validate_name accepted name with invalid chars: '{name}'" ) @given(name=invalid_names_too_short()) @settings(max_examples=50) def test_names_too_short_are_rejected(self, name: str) -> None: """Names shorter than 2 characters must be rejected.""" assert manager.validate_name(name) is False, ( f"validate_name accepted too-short name: '{name}'" ) @given(name=invalid_names_too_long()) @settings(max_examples=200) def test_names_too_long_are_rejected(self, name: str) -> None: """Names longer than 50 characters must be rejected.""" assert manager.validate_name(name) is False, ( f"validate_name accepted too-long name: '{name}'" ) @given(name=invalid_names_leading_dash()) @settings(max_examples=100) def test_names_starting_with_dash_are_rejected(self, name: str) -> None: """Names starting with a dash must be rejected.""" assert manager.validate_name(name) is False, ( f"validate_name accepted name starting with dash: '{name}'" ) @given(name=invalid_names_trailing_dash()) @settings(max_examples=100) def test_names_ending_with_dash_are_rejected(self, name: str) -> None: """Names ending with a dash must be rejected.""" assert manager.validate_name(name) is False, ( f"validate_name accepted name ending with dash: '{name}'" ) @given(name=st.text(min_size=0, max_size=100)) @settings(max_examples=500) def test_validate_name_biconditional(self, name: str) -> None: """For any string, validate_name returns True if and only if: - The string consists exclusively of [a-z0-9-] - Has length between 2 and 50 (inclusive) - Does not start or end with a dash This is the full biconditional property check. """ # Compute expected result from first principles is_valid_chars = all(c in VALID_CHARS for c in name) is_valid_length = 2 <= len(name) <= 50 no_leading_dash = len(name) > 0 and name[0] != "-" no_trailing_dash = len(name) > 0 and name[-1] != "-" expected = ( is_valid_chars and is_valid_length and no_leading_dash and no_trailing_dash ) result = manager.validate_name(name) assert result == expected, ( f"validate_name('{name}') returned {result}, expected {expected}. " f"chars_valid={is_valid_chars}, length_valid={is_valid_length}, " f"no_lead_dash={no_leading_dash}, no_trail_dash={no_trailing_dash}" )