Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2be55d46e | ||
|
|
b3ccc56eff |
@@ -6,6 +6,25 @@ This module validates an inbound XML invoice through a locally installed, exact
|
||||
|
||||
An administrator records the XRechnung version, validator version, configuration release, absolute Java and JAR paths, JAR SHA-256, configuration root and complete deterministic tree SHA-256, scenario file, and expected minimum validation-step count. Every run rechecks the executable and both artifact digests. Symbolic links and files outside the configuration root are rejected.
|
||||
|
||||
## Deployment-owned profile allow-list
|
||||
|
||||
`KoSITProfileRegistry` allows a deployment to approve several exact profiles
|
||||
without GovOPlaN choosing a standard version. Each approval binds the verified
|
||||
artifact digest, approving actor, decision reference, receive-time window, and
|
||||
the state `approved`, `suspended`, or `retired`. A deployment may configure one
|
||||
explicit default, or require the invoking workflow to select a profile every
|
||||
time. A default is never inferred.
|
||||
|
||||
Selection uses the invoice receive time, which permits an administrator to
|
||||
model an overlap or transition window. Suspended, retired, unknown, expired,
|
||||
not-yet-valid, or subsequently modified profiles fail closed. Artifact digests
|
||||
are rechecked on selection and again during validation.
|
||||
|
||||
Administrierende können damit mehrere konkrete Profile zeitlich begrenzt
|
||||
freigeben, ohne dass GovOPlaN eine XRechnungs-Version vorgibt. Übergangsfristen
|
||||
und ein Standardprofil sind ausdrücklich konfigurierte Entscheidungen. Ohne
|
||||
Standard muss jeder aufrufende Prozess ein freigegebenes Profil benennen.
|
||||
|
||||
The configuration tree must be installed through deployment governance. A newer published release never becomes active automatically. Updating any artifact creates a different profile digest and requires regression evidence with accepted, rejected, malformed, and deliberately broken technical fixtures.
|
||||
|
||||
## Fail-closed validation
|
||||
|
||||
+1
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-xrechnung"
|
||||
version = "0.1.19"
|
||||
version = "0.1.21"
|
||||
description = "GovOPlaN inbound XRechnung validation connector."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
@@ -30,4 +30,3 @@ pythonpath = ["src"]
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"""GovOPlaN XRechnung connector."""
|
||||
|
||||
__version__ = "0.1.19"
|
||||
|
||||
__version__ = "0.1.21"
|
||||
|
||||
@@ -16,7 +16,7 @@ from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
|
||||
|
||||
MODULE_ID = "xrechnung"
|
||||
MODULE_VERSION = "0.1.19"
|
||||
MODULE_VERSION = "0.1.21"
|
||||
READ_SCOPE = "xrechnung:validation:read"
|
||||
EXECUTE_SCOPE = "xrechnung:validation:execute"
|
||||
HANDOFF_SCOPE = "xrechnung:handoff:create"
|
||||
@@ -71,7 +71,7 @@ manifest = ModuleManifest(
|
||||
title="Validate and hand off an inbound XRechnung",
|
||||
summary="Verify safe invoice XML with an exact locally pinned KoSIT engine and rule tree, then create a digest-bound handoff only from complete valid evidence.",
|
||||
body=(
|
||||
"An administrator must approve an exact XRechnung version, KoSIT validator release, configuration release, configuration-tree digest, engine digest, scenario file, and minimum validation-step count. The connector verifies those artifacts before each run and never downloads rules while processing an invoice. It rejects dangerous XML before invoking Java. Runner exit, bounded output, technical error markers, VARL identity, matched scenario, configured validation-step count, validity flag, and exactly one accept or reject assessment are checked independently. Technical failure or partial reports yield unknown conformance and can never be handed off. A valid handoff binds tenant, source reference, invoice digest, report digest, and profile digest for an owning Procurement or Payments workflow."
|
||||
"An administrator must approve an exact XRechnung version, KoSIT validator release, configuration release, configuration-tree digest, engine digest, scenario file, and minimum validation-step count. A deployment-owned allow-list records approval identity, decision reference, receive-time window, suspension or retirement state, and an optional explicit default; GovOPlaN selects no version itself. The connector verifies those artifacts before each run and never downloads rules while processing an invoice. It rejects dangerous XML before invoking Java. Runner exit, bounded output, technical error markers, VARL identity, matched scenario, configured validation-step count, validity flag, and exactly one accept or reject assessment are checked independently. Technical failure or partial reports yield unknown conformance and can never be handed off. A valid handoff binds tenant, source reference, invoice digest, report digest, and profile digest for an owning Procurement or Payments workflow."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -93,7 +93,7 @@ manifest = ModuleManifest(
|
||||
"de": {
|
||||
"title": "Eingehende XRechnung prüfen und übergeben",
|
||||
"summary": "Sicheres Rechnungs-XML mit exakt lokal festgelegter KoSIT-Engine und Regelstruktur prüfen und nur aus vollständigem gültigem Nachweis eine prüfsummengebundene Übergabe erzeugen.",
|
||||
"body": "Administrierende müssen eine exakte XRechnungs-Version, KoSIT-Validator-Version, Konfigurationsversion, Prüfsumme der Konfigurationsstruktur, Engine-Prüfsumme, Szenariodatei und Mindestzahl der Prüfschritte freigeben. Der Konnektor prüft diese Artefakte vor jedem Lauf und lädt während der Rechnungsverarbeitung keine Regeln herunter. Gefährliches XML wird vor dem Java-Aufruf abgewiesen. Prozessende, begrenzte Ausgabe, technische Fehlermarker, VARL-Identität, erkanntes Szenario, konfigurierte Prüfschrittzahl, Gültigkeitskennzeichen und genau eine Annahme- oder Ablehnungsempfehlung werden unabhängig geprüft. Technische Fehler oder Teilberichte ergeben unbekannte Konformität und dürfen niemals übergeben werden. Eine gültige Übergabe bindet Mandant, Quellreferenz, Rechnungsprüfsumme, Berichtsprüfsumme und Profilprüfsumme für einen fachlich verantwortlichen Procurement- oder Payments-Ablauf.",
|
||||
"body": "Administrierende müssen eine exakte XRechnungs-Version, KoSIT-Validator-Version, Konfigurationsversion, Prüfsumme der Konfigurationsstruktur, Engine-Prüfsumme, Szenariodatei und Mindestzahl der Prüfschritte freigeben. Eine installationsbezogene Positivliste erfasst Freigabestelle, Entscheidungsreferenz, Eingangszeitfenster, Sperrung oder Ausmusterung und einen optional ausdrücklich gesetzten Standard; GovOPlaN wählt selbst keine Version. Der Konnektor prüft die Artefakte vor jedem Lauf und lädt während der Rechnungsverarbeitung keine Regeln herunter. Gefährliches XML wird vor dem Java-Aufruf abgewiesen. Prozessende, begrenzte Ausgabe, technische Fehlermarker, VARL-Identität, erkanntes Szenario, konfigurierte Prüfschrittzahl, Gültigkeitskennzeichen und genau eine Annahme- oder Ablehnungsempfehlung werden unabhängig geprüft. Technische Fehler oder Teilberichte ergeben unbekannte Konformität und dürfen niemals übergeben werden. Eine gültige Übergabe bindet Mandant, Quellreferenz, Rechnungsprüfsumme, Berichtsprüfsumme und Profilprüfsumme für einen fachlich verantwortlichen Procurement- oder Payments-Ablauf.",
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
@@ -101,10 +101,12 @@ manifest = ModuleManifest(
|
||||
"prerequisites": [
|
||||
"An exact KoSIT engine and self-contained XRechnung configuration have been installed locally.",
|
||||
"The administrator has approved all artifact digests and the expected validation-step count.",
|
||||
"The profile is approved for the invoice receive time in the deployment allow-list.",
|
||||
"The owning invoice workflow supplies a stable tenant and source reference.",
|
||||
],
|
||||
"steps": [
|
||||
"Verify the executable, engine digest, complete configuration-tree digest, and scenario location.",
|
||||
"Select an explicit allow-listed profile or the deployment's explicitly configured default.",
|
||||
"Reject unsafe or oversized XML, then run the pinned profile with bounded time and output.",
|
||||
"Check technical completeness separately from the report's semantic validity.",
|
||||
"Create a handoff only when the report is complete, formally valid, and accepts the invoice.",
|
||||
@@ -121,8 +123,105 @@ manifest = ModuleManifest(
|
||||
],
|
||||
"verification": "Confirm artifact and profile digests, technical outcome, validation-step count, formal validity, VARL assessment, invoice digest, and report digest before following the handoff reference.",
|
||||
},
|
||||
structured_translation_version="1",
|
||||
structured_translations={
|
||||
"de": {
|
||||
"prerequisites": [
|
||||
"Eine exakte KoSIT-Engine und eine eigenständige XRechnung-Konfiguration sind lokal installiert.",
|
||||
"Die Administration hat alle Artefaktprüfsummen und die erwartete Zahl der Prüfschritte freigegeben.",
|
||||
"Das Profil ist in der installationsbezogenen Positivliste für den Eingangszeitpunkt der Rechnung freigegeben.",
|
||||
"Der fachlich verantwortliche Rechnungsablauf liefert einen stabilen Mandanten- und Quellverweis.",
|
||||
],
|
||||
"steps": [
|
||||
"Ausführbare Datei, Engine-Prüfsumme, vollständige Prüfsumme der Konfigurationsstruktur und Szenariopfad prüfen.",
|
||||
"Ein ausdrückliches Positivlistenprofil oder den ausdrücklich konfigurierten Installationsstandard auswählen.",
|
||||
"Unsicheres oder übergroßes XML abweisen und anschließend das festgelegte Profil mit begrenzter Zeit und Ausgabe ausführen.",
|
||||
"Technische Vollständigkeit getrennt von der semantischen Gültigkeit des Berichts prüfen.",
|
||||
"Nur bei vollständigem, formal gültigem und die Rechnung annehmendem Bericht eine Übergabe erzeugen.",
|
||||
],
|
||||
"limitations": [
|
||||
"Diese Version aktiviert standardmäßig keine XRechnung- oder Konfigurationsversion.",
|
||||
"Der Konnektor erzeugt einen Übergabevertrag, führt aber weder Freigabe der Verbindlichkeit noch Buchung.",
|
||||
"Ein syntaktisch gültiger Bericht ist nicht vertrauenswürdig, wenn die Runner-Ausgabe einen technischen Fehler meldet.",
|
||||
],
|
||||
"consequences": [
|
||||
"Jede Änderung eines festgelegten Artefakts ändert die Profilprüfsumme und erfordert eine neue Validierung.",
|
||||
"Ungültige Rechnungen bewahren Diagnosen, dürfen aber nicht in die gültige Rechnungsübergabe gelangen.",
|
||||
"Technischer Fehler ergibt unbekannte Konformität, niemals semantische Ablehnung oder Annahme.",
|
||||
],
|
||||
"verification": "Vor dem Folgen des Übergabeverweises Artefakt- und Profilprüfsummen, technisches Ergebnis, Prüfschrittzahl, formale Gültigkeit, VARL-Bewertung, Rechnungsprüfsumme und Berichtsprüfsumme bestätigen.",
|
||||
}
|
||||
},
|
||||
order=100,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="xrechnung.reference.validation-profile-and-handoff",
|
||||
title="XRechnung validation-profile and handoff consequences",
|
||||
summary=(
|
||||
"Understand receive-time profile selection, immutable artifact approval, "
|
||||
"technical uncertainty, and the evidence boundary of a valid-invoice handoff."
|
||||
),
|
||||
body=(
|
||||
"A validation profile names exact engine, configuration, scenario, and artifact "
|
||||
"digests. Its approval has an actor, decision reference, receive-time window, and "
|
||||
"approved, suspended, or retired state. Selection uses the invoice receive time and "
|
||||
"never infers a product version. Unknown, not-yet-valid, expired, suspended, retired, "
|
||||
"or digest-mismatched profiles fail closed. Validation evidence distinguishes runner "
|
||||
"completion, report completeness, formal validity, and semantic assessment. A handoff "
|
||||
"binds the exact invoice, report, and profile digests but does not approve a payable, "
|
||||
"book an invoice, retain the source file, or replace Records custody."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("user", "operator", "module_admin", "auditor"),
|
||||
related_modules=("files", "procurement", "payments", "records", "audit"),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="Inbound validation profile",
|
||||
href="docs/INBOUND_VALIDATION.md",
|
||||
kind="repository",
|
||||
),
|
||||
),
|
||||
translations={
|
||||
"de": {
|
||||
"title": "Folgen von XRechnung-Prüfprofil und Übergabe",
|
||||
"summary": (
|
||||
"Profilauswahl nach Eingangszeit, unveränderliche Artefaktfreigabe, technische Ungewissheit und die Nachweisgrenze einer gültigen Rechnungsübergabe verstehen."
|
||||
),
|
||||
"body": (
|
||||
"Ein Prüfprofil bezeichnet exakte Prüfsummen von Engine, Konfiguration, Szenario und Artefakten. Seine "
|
||||
"Freigabe enthält Akteur, Entscheidungsverweis, Eingangszeitfenster und den Zustand freigegeben, gesperrt "
|
||||
"oder ausgemustert. Die Auswahl verwendet den Eingangszeitpunkt der Rechnung und leitet niemals eine "
|
||||
"Produktversion ab. Unbekannte, noch nicht gültige, abgelaufene, gesperrte, ausgemusterte oder bei der "
|
||||
"Prüfsumme abweichende Profile werden geschlossen abgewiesen. Der Validierungsnachweis unterscheidet "
|
||||
"Runner-Abschluss, Berichtsvollständigkeit, formale Gültigkeit und semantische Bewertung. Eine Übergabe "
|
||||
"bindet exakte Rechnungs-, Berichts- und Profilprüfsummen, genehmigt aber keine Verbindlichkeit, bucht keine "
|
||||
"Rechnung, bewahrt keine Quelldatei und ersetzt nicht die Verwahrung durch Records."
|
||||
),
|
||||
}
|
||||
},
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"consequence_classes": {
|
||||
"approve_profile": "Makes one exact verified artifact set eligible only in its receive-time window.",
|
||||
"suspend_or_retire_profile": "Blocks selection without rewriting prior validation evidence.",
|
||||
"fail_closed": "Treats unknown profile or technical outcome as untrusted rather than valid or invalid.",
|
||||
"create_handoff": "Binds valid evidence for an owner workflow without approving, booking, or storing the invoice.",
|
||||
},
|
||||
},
|
||||
structured_translation_version="1",
|
||||
structured_translations={
|
||||
"de": {
|
||||
"consequence_classes": {
|
||||
"approve_profile": "Macht genau einen geprüften Artefaktsatz ausschließlich in seinem Eingangszeitfenster auswählbar.",
|
||||
"suspend_or_retire_profile": "Sperrt die Auswahl, ohne frühere Validierungsnachweise umzuschreiben.",
|
||||
"fail_closed": "Behandelt unbekanntes Profil oder technisches Ergebnis als nicht vertrauenswürdig statt als gültig oder ungültig.",
|
||||
"create_handoff": "Bindet gültige Nachweise für einen Eigentümerablauf, ohne die Rechnung zu genehmigen, zu buchen oder zu speichern.",
|
||||
}
|
||||
}
|
||||
},
|
||||
order=110,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="data_reporting_integration",
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from govoplan_xrechnung.backend.validation import (
|
||||
KoSITValidationProfile,
|
||||
XRechnungValidationError,
|
||||
verify_profile,
|
||||
)
|
||||
|
||||
|
||||
ProfileApprovalStatus = Literal["approved", "suspended", "retired"]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KoSITProfileApproval:
|
||||
"""A reviewed deployment decision bound to exact installed artifacts."""
|
||||
|
||||
profile: KoSITValidationProfile
|
||||
profile_sha256: str
|
||||
status: ProfileApprovalStatus
|
||||
approved_at: datetime
|
||||
approved_by: str
|
||||
decision_reference: str
|
||||
accept_from: datetime
|
||||
accept_until: datetime | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.status not in {"approved", "suspended", "retired"}:
|
||||
raise ValueError("XRechnung profile approval status is unsupported.")
|
||||
for name in ("approved_by", "decision_reference"):
|
||||
value = str(getattr(self, name) or "").strip()
|
||||
if not value or len(value) > 255 or any(ord(char) < 32 for char in value):
|
||||
raise ValueError(
|
||||
f"XRechnung {name.replace('_', ' ')} is required and bounded."
|
||||
)
|
||||
object.__setattr__(self, name, value)
|
||||
for name in ("approved_at", "accept_from"):
|
||||
_aware(getattr(self, name), name)
|
||||
if self.accept_until is not None:
|
||||
_aware(self.accept_until, "accept_until")
|
||||
if self.accept_until <= self.accept_from:
|
||||
raise ValueError("XRechnung accept_until must be later than accept_from.")
|
||||
normalized_digest = str(self.profile_sha256 or "").strip().lower()
|
||||
if len(normalized_digest) != 64 or any(
|
||||
character not in "0123456789abcdef" for character in normalized_digest
|
||||
):
|
||||
raise ValueError("XRechnung approved profile digest must be SHA-256.")
|
||||
object.__setattr__(self, "profile_sha256", normalized_digest)
|
||||
|
||||
@classmethod
|
||||
def approve(
|
||||
cls,
|
||||
profile: KoSITValidationProfile,
|
||||
*,
|
||||
approved_at: datetime,
|
||||
approved_by: str,
|
||||
decision_reference: str,
|
||||
accept_from: datetime,
|
||||
accept_until: datetime | None = None,
|
||||
) -> KoSITProfileApproval:
|
||||
return cls(
|
||||
profile=profile,
|
||||
profile_sha256=verify_profile(profile),
|
||||
status="approved",
|
||||
approved_at=approved_at,
|
||||
approved_by=approved_by,
|
||||
decision_reference=decision_reference,
|
||||
accept_from=accept_from,
|
||||
accept_until=accept_until,
|
||||
)
|
||||
|
||||
def accepts(self, received_at: datetime) -> bool:
|
||||
_aware(received_at, "invoice received_at")
|
||||
return (
|
||||
self.status == "approved"
|
||||
and received_at >= self.accept_from
|
||||
and (self.accept_until is None or received_at < self.accept_until)
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KoSITProfileRegistry:
|
||||
"""Deployment-owned allow-list with an optional, explicit default profile."""
|
||||
|
||||
approvals: tuple[KoSITProfileApproval, ...]
|
||||
default_profile_id: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.approvals:
|
||||
raise ValueError("XRechnung profile registry requires at least one reviewed entry.")
|
||||
profile_ids = [item.profile.profile_id for item in self.approvals]
|
||||
if len(profile_ids) != len(set(profile_ids)):
|
||||
raise ValueError("XRechnung profile registry contains duplicate profile ids.")
|
||||
profile_digests = [item.profile_sha256 for item in self.approvals]
|
||||
if len(profile_digests) != len(set(profile_digests)):
|
||||
raise ValueError("XRechnung profile registry contains duplicate artifact profiles.")
|
||||
if self.default_profile_id is not None:
|
||||
normalized = str(self.default_profile_id or "").strip()
|
||||
if normalized not in profile_ids:
|
||||
raise ValueError("XRechnung default profile is not present in the allow-list.")
|
||||
object.__setattr__(self, "default_profile_id", normalized)
|
||||
|
||||
def select(
|
||||
self,
|
||||
*,
|
||||
received_at: datetime,
|
||||
profile_id: str | None = None,
|
||||
) -> KoSITValidationProfile:
|
||||
selected_id = str(profile_id or self.default_profile_id or "").strip()
|
||||
if not selected_id:
|
||||
raise XRechnungValidationError(
|
||||
"No XRechnung profile was selected and the deployment has no default."
|
||||
)
|
||||
approval = next(
|
||||
(item for item in self.approvals if item.profile.profile_id == selected_id),
|
||||
None,
|
||||
)
|
||||
if approval is None:
|
||||
raise XRechnungValidationError(
|
||||
"The selected XRechnung profile is not in the deployment allow-list."
|
||||
)
|
||||
if not approval.accepts(received_at):
|
||||
raise XRechnungValidationError(
|
||||
"The selected XRechnung profile is not approved for the invoice receive time."
|
||||
)
|
||||
observed_digest = verify_profile(approval.profile)
|
||||
if observed_digest != approval.profile_sha256:
|
||||
raise XRechnungValidationError(
|
||||
"The installed XRechnung profile no longer matches its approval evidence."
|
||||
)
|
||||
return approval.profile
|
||||
|
||||
|
||||
def _aware(value: datetime, label: str) -> None:
|
||||
if value.tzinfo is None or value.utcoffset() is None:
|
||||
raise ValueError(f"XRechnung {label.replace('_', ' ')} must be timezone-aware.")
|
||||
|
||||
|
||||
__all__ = ["KoSITProfileApproval", "KoSITProfileRegistry", "ProfileApprovalStatus"]
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.modules import documentation_structured_translation_issues
|
||||
from govoplan_xrechnung.backend.manifest import manifest
|
||||
|
||||
|
||||
def test_xrechnung_german_workflow_and_reference_are_complete() -> None:
|
||||
topics = {topic.id: topic for topic in manifest.documentation}
|
||||
assert len(topics) == 2
|
||||
assert topics["xrechnung.inbound-validation"].metadata["kind"] == "workflow"
|
||||
assert topics["xrechnung.reference.validation-profile-and-handoff"].metadata["kind"] == "reference"
|
||||
for topic in topics.values():
|
||||
assert all(topic.translations["de"].get(key) for key in ("title", "summary", "body"))
|
||||
assert topic.structured_translation_version == "1"
|
||||
assert "de" in topic.structured_translations
|
||||
assert documentation_structured_translation_issues(topic) == ()
|
||||
@@ -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.21"
|
||||
assert "none is activated by default" in manifest.architecture.known_limits[0].lower()
|
||||
|
||||
Reference in New Issue
Block a user