"""Property-based tests for the Summary Reporter module using Hypothesis.""" from __future__ import annotations from hypothesis import given, settings, assume from hypothesis import strategies as st from scripts.pat_manager.models import CheckResult, PatEntry, PatStatus from scripts.pat_manager.reporter import SummaryReporter # --- Strategies --- _SERVICES_AND_METHODS = [ ("GitLab", "auto"), ("Jira", "manual"), ("Confluence", "manual"), ("OrgMyLife", "manual"), ] _ALL_STATUSES = [ PatStatus.HEALTHY, PatStatus.EXPIRING_SOON, PatStatus.EXPIRED, PatStatus.CHECK_FAILED, ] @st.composite def simple_token_names(draw: st.DrawFn) -> str: """Generate simple token names using alphanumeric + hyphens for reliable report matching.""" name = draw( st.text( alphabet=st.characters( whitelist_categories=("Ll", "Lu", "Nd"), whitelist_characters="-", ), min_size=1, max_size=64, ) ) # Ensure name starts with a letter (not hyphen) and is non-empty after strip assume(len(name.strip()) > 0) assume(name[0].isalpha()) return name @st.composite def valid_iso_dates(draw: st.DrawFn) -> str: """Generate valid ISO 8601 date strings (YYYY-MM-DD).""" year = draw(st.integers(min_value=2020, max_value=2099)) month = draw(st.integers(min_value=1, max_value=12)) if month in (1, 3, 5, 7, 8, 10, 12): max_day = 31 elif month in (4, 6, 9, 11): max_day = 30 elif month == 2: if year % 4 == 0 and (year % 100 != 0 or year % 400 == 0): max_day = 29 else: max_day = 28 else: max_day = 28 day = draw(st.integers(min_value=1, max_value=max_day)) return f"{year:04d}-{month:02d}-{day:02d}" @st.composite def check_results(draw: st.DrawFn) -> CheckResult: """Generate a CheckResult with a random status and appropriate fields.""" service, renewal_method = draw(st.sampled_from(_SERVICES_AND_METHODS)) token_name = draw(simple_token_names()) expiry_date = draw(st.one_of(st.none(), valid_iso_dates())) status = draw(st.sampled_from(_ALL_STATUSES)) # Generate appropriate days_remaining and error_message based on status if status == PatStatus.CHECK_FAILED: days_remaining = None error_message = draw( st.text( alphabet=st.characters(whitelist_categories=("L", "N", "Z")), min_size=3, max_size=50, ) ) elif status == PatStatus.EXPIRED: days_remaining = 0 error_message = None elif status == PatStatus.EXPIRING_SOON: days_remaining = draw(st.integers(min_value=1, max_value=14)) error_message = None else: # HEALTHY days_remaining = draw(st.integers(min_value=15, max_value=365)) error_message = None entry = PatEntry( service=service, token_name=token_name, expiry_date=expiry_date, renewal_method=renewal_method, ) return CheckResult( entry=entry, status=status, days_remaining=days_remaining, error_message=error_message, ) # --- Property 11 Test --- # Feature: pat-renewal, Property 11: Summary report completeness class TestSummaryReportCompleteness: """ **Validates: Requirements 5.3** Property 11: For any list of check results, the generated summary report SHALL contain one line per result, and each line SHALL include the service name, token identifier, and classification status. """ @given(results=st.lists(check_results(), min_size=1, max_size=10)) @settings(max_examples=100) def test_report_contains_service_token_and_status_for_each_result( self, results: list[CheckResult] ) -> None: """For each check result, the report contains the service name, token name, and status value.""" reporter = SummaryReporter() report = reporter.report(results, []) for result in results: # The report must contain the service name assert result.entry.service in report, ( f"Service '{result.entry.service}' not found in report" ) # The report must contain the token name assert result.entry.token_name in report, ( f"Token name '{result.entry.token_name}' not found in report" ) # The report must contain the status value assert result.status.value in report, ( f"Status '{result.status.value}' not found in report" )