Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
146 lines
4.6 KiB
Python
146 lines
4.6 KiB
Python
"""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"
|
|
)
|