feat(xrechnung): govern validation profile selection
Module Package Release / publish-packages (push) Successful in 10s
Module Package Release / publish-packages (push) Successful in 10s
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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("<scenarios/>", 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")
|
||||
@@ -154,5 +154,5 @@ def test_technical_failures_never_trust_a_valid_looking_report(
|
||||
|
||||
def test_manifest_does_not_select_an_active_standard_version() -> None:
|
||||
manifest = get_manifest()
|
||||
assert manifest.version == "0.1.19"
|
||||
assert manifest.version == "0.1.20"
|
||||
assert "none is activated by default" in manifest.architecture.known_limits[0].lower()
|
||||
|
||||
Reference in New Issue
Block a user