from __future__ import annotations from dataclasses import replace from datetime import UTC, datetime import hashlib from pathlib import Path import pytest from govoplan_xrechnung.backend.profiles import KoSITProfileApproval, KoSITProfileRegistry from govoplan_xrechnung.backend.validation import ( KoSITValidationProfile, XRechnungValidationError, configuration_tree_sha256, ) def _profile(tmp_path: Path, profile_id: str = "profile-a") -> KoSITValidationProfile: root = tmp_path / profile_id root.mkdir() java = root / "java" java.write_bytes(b"#!/bin/sh\n") java.chmod(0o700) jar = root / "validator.jar" jar.write_bytes(b"validator") config = root / "config" config.mkdir() scenarios = config / "scenarios.xml" scenarios.write_text("", encoding="utf-8") return KoSITValidationProfile( profile_id=profile_id, xrechnung_version="configured-version", validator_version="configured-validator", configuration_version="configured-rules", java_executable=java.resolve(), validator_jar=jar.resolve(), validator_jar_sha256=hashlib.sha256(jar.read_bytes()).hexdigest(), configuration_root=config.resolve(), configuration_tree_sha256=configuration_tree_sha256(config), scenarios_file=scenarios.resolve(), minimum_validation_steps=1, ) def _approval(tmp_path: Path) -> KoSITProfileApproval: return KoSITProfileApproval.approve( _profile(tmp_path), approved_at=datetime(2026, 8, 23, tzinfo=UTC), approved_by="configuration-board", decision_reference="decision:xrechnung-profile-a", accept_from=datetime(2026, 9, 1, tzinfo=UTC), accept_until=datetime(2027, 1, 1, tzinfo=UTC), ) def test_registry_requires_explicit_selection_without_default(tmp_path: Path) -> None: registry = KoSITProfileRegistry((_approval(tmp_path),)) with pytest.raises(XRechnungValidationError, match="no default"): registry.select(received_at=datetime(2026, 10, 1, tzinfo=UTC)) def test_explicit_or_configured_default_selects_only_inside_window(tmp_path: Path) -> None: approval = _approval(tmp_path) registry = KoSITProfileRegistry( (approval,), default_profile_id=approval.profile.profile_id, ) selected = registry.select(received_at=datetime(2026, 10, 1, tzinfo=UTC)) assert selected.profile_id == "profile-a" with pytest.raises(XRechnungValidationError, match="receive time"): registry.select(received_at=datetime(2027, 1, 1, tzinfo=UTC)) @pytest.mark.parametrize("status", ["suspended", "retired"]) def test_non_approved_profile_cannot_be_selected(tmp_path: Path, status: str) -> None: approval = replace(_approval(tmp_path), status=status) registry = KoSITProfileRegistry((approval,), default_profile_id="profile-a") with pytest.raises(XRechnungValidationError, match="not approved"): registry.select(received_at=datetime(2026, 10, 1, tzinfo=UTC)) def test_changed_artifact_is_rejected_after_approval(tmp_path: Path) -> None: approval = _approval(tmp_path) registry = KoSITProfileRegistry((approval,), default_profile_id="profile-a") approval.profile.validator_jar.write_bytes(b"changed") with pytest.raises(XRechnungValidationError, match="digest"): registry.select(received_at=datetime(2026, 10, 1, tzinfo=UTC)) def test_default_must_reference_allow_list(tmp_path: Path) -> None: with pytest.raises(ValueError, match="not present"): KoSITProfileRegistry((_approval(tmp_path),), default_profile_id="missing")