Files
zemion b3ccc56eff
Module Package Release / publish-packages (push) Successful in 10s
feat(xrechnung): govern validation profile selection
2026-08-23 17:53:23 +02:00

98 lines
3.6 KiB
Python

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")