"""CRA Art. 14 reporting cascade — pure deadline + ENISA-export logic.""" import pytest from compliance.services.cra_meldewesen import ( compute_deadlines, next_open_stage, build_enisa_report, report_completeness, ) AWARE = "2026-09-15T08:00:00+00:00" class TestDeadlines: def test_due_times_are_24h_72h_14d_after_awareness(self): d = compute_deadlines(AWARE, now_iso=AWARE) by = {x["key"]: x for x in d} assert by["early_warning"]["due_at"] == "2026-09-16T08:00:00+00:00" assert by["notification"]["due_at"] == "2026-09-18T08:00:00+00:00" assert by["final"]["due_at"] == "2026-09-29T08:00:00+00:00" def test_all_pending_right_after_awareness(self): d = compute_deadlines(AWARE, now_iso="2026-09-15T09:00:00+00:00") assert all(x["status"] == "pending" for x in d) def test_overdue_when_now_past_due_and_unsubmitted(self): d = compute_deadlines(AWARE, now_iso="2026-09-17T00:00:00+00:00") by = {x["key"]: x for x in d} assert by["early_warning"]["status"] == "overdue" # 24h passed assert by["notification"]["status"] == "pending" # 72h not yet def test_due_soon_within_window(self): # 4h before the 24h deadline → due_soon d = compute_deadlines(AWARE, now_iso="2026-09-16T04:00:00+00:00") by = {x["key"]: x for x in d} assert by["early_warning"]["status"] == "due_soon" def test_submitted_overrides_timing(self): d = compute_deadlines( AWARE, submissions={"early_warning": "2026-09-16T07:00:00+00:00"}, now_iso="2026-09-20T00:00:00+00:00") by = {x["key"]: x for x in d} assert by["early_warning"]["status"] == "submitted" assert by["notification"]["status"] == "overdue" def test_next_open_stage(self): d = compute_deadlines(AWARE, submissions={"early_warning": AWARE}, now_iso=AWARE) assert next_open_stage(d) == "notification" d2 = compute_deadlines( AWARE, submissions={k: AWARE for k in ("early_warning", "notification", "final")}, now_iso=AWARE) assert next_open_stage(d2) is None class TestEnisaReport: def _incident(self): return { "manufacturer": "OWIS GmbH", "product_name": "PS 90+", "product_version": "2.1", "kind": "exploited_vulnerability", "severity": "high", "aware_at": AWARE, "summary": "Aktiv ausgenutzte Auth-Umgehung", "contact": "psirt@owis.eu", "impact": "Fernsteuerung möglich", "root_cause": "fehlende Tokenprüfung", "patch_available": True, } def test_early_warning_has_base_not_detail(self): r = build_enisa_report(self._incident(), "early_warning") assert r["manufacturer"] == "OWIS GmbH" and r["severity"] == "high" assert "impact" not in r and "root_cause" not in r assert r["submission_target"].startswith("ENISA") def test_notification_adds_impact_not_rootcause(self): r = build_enisa_report(self._incident(), "notification") assert r["impact"] == "Fernsteuerung möglich" assert "root_cause" not in r def test_final_adds_root_cause_and_patch(self): r = build_enisa_report(self._incident(), "final") assert r["root_cause"] == "fehlende Tokenprüfung" assert r["patch_available"] is True def test_unknown_stage_raises(self): with pytest.raises(ValueError): build_enisa_report(self._incident(), "nonsense") def test_completeness_flags_missing(self): thin = {"manufacturer": "X", "aware_at": AWARE} c = report_completeness(thin, "early_warning") assert not c["complete"] assert "product_name" in c["missing"] assert "manufacturer" in c["filled"]