b9baa8c603
Erkennt B2C-Shop ohne öffentlich erreichbare Widerrufsbelehrung.
Schließt eine der offenen GT-Lücken aus dem Elli-Audit.
Signale:
- doc_entries[widerruf]: discovery_attempted=True + Text leer
- kein Footer-Link auf Widerruf/cancellation/rückgabe
- B2C-Scope: Warenkorb/Kasse/Bestellung/MwSt/Wallbox/Tarif (strong)
vs Shop/Produkt/Rechnung (weak, ≥2 = likely)
- B2B-only-Override: "ausschließlich an Unternehmer" etc.
Severity:
- HIGH bei b2c_strong
- MEDIUM bei b2c_likely
- kein Finding bei b2b_only / unknown (False-Positive-Schutz)
Norm: Art. 246a § 1 Abs. 2 Nr. 1 EGBGB i.V.m. § 312d BGB.
Wiring:
- widerrufsbelehrung_reachability_check.py — Check + Scope-Detection
- _b13_wiring.py — Render + state-Anschluss
- _orchestrator.py — run_b13 nach run_b12
- mail_render_v2/_compose.py — widerruf_reach_html-Block
Tests: 13/13 grün (Scope-Detection 5 + Check-Logik 8).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.8 KiB
Python
138 lines
4.8 KiB
Python
"""Tests for B13 Widerrufsbelehrung-Reachability-Check."""
|
|
|
|
from compliance.services.widerrufsbelehrung_reachability_check import (
|
|
_detect_b2c_scope,
|
|
check_widerrufsbelehrung_reachability,
|
|
)
|
|
|
|
|
|
def _state(widerruf_entry=None, home_text="", doc_entries=None,
|
|
footer_text=""):
|
|
entries = list(doc_entries or [])
|
|
if widerruf_entry is not None:
|
|
entries.append({"doc_type": "widerruf", **widerruf_entry})
|
|
return {
|
|
"doc_entries": entries,
|
|
"home_text": home_text,
|
|
"footer_text": footer_text,
|
|
}
|
|
|
|
|
|
class TestDetectB2CScope:
|
|
def test_strong_b2c_warenkorb(self):
|
|
s = _state(home_text="Legen Sie das Produkt in den Warenkorb.")
|
|
scope, hits = _detect_b2c_scope(s)
|
|
assert scope == "b2c_strong"
|
|
assert any("warenkorb" in h for h in hits)
|
|
|
|
def test_b2b_only_overrides_b2c(self):
|
|
s = _state(home_text="Wir verkaufen ausschließlich an Unternehmer. "
|
|
"Warenkorb für Großkunden.")
|
|
scope, _ = _detect_b2c_scope(s)
|
|
assert scope == "b2b_only"
|
|
|
|
def test_weak_signals_two_promote_to_likely(self):
|
|
s = _state(home_text="Unser Shop bietet Wallbox-Produkte mit "
|
|
"Rechnung zur Bestellung.")
|
|
scope, _ = _detect_b2c_scope(s)
|
|
assert scope == "b2c_likely"
|
|
|
|
def test_single_weak_signal_stays_unknown(self):
|
|
s = _state(home_text="Wir bieten einen Shop.")
|
|
scope, _ = _detect_b2c_scope(s)
|
|
assert scope == "unknown"
|
|
|
|
def test_empty_state(self):
|
|
s = _state()
|
|
scope, _ = _detect_b2c_scope(s)
|
|
assert scope == "unknown"
|
|
|
|
|
|
class TestCheck:
|
|
def test_no_widerruf_entry_no_finding(self):
|
|
out = check_widerrufsbelehrung_reachability(_state())
|
|
assert out == []
|
|
|
|
def test_widerruf_reachable_no_finding(self):
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "https://shop.de/widerruf",
|
|
"text": "Belehrung " * 100,
|
|
"discovery_attempted": True,
|
|
},
|
|
home_text="Warenkorb / zur Kasse.",
|
|
))
|
|
assert out == []
|
|
|
|
def test_unreachable_plus_b2c_strong_high_finding(self):
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": True,
|
|
"rejected_url": "https://shop.de/widerruf",
|
|
},
|
|
home_text="In den Warenkorb. Lieferzeit 2 Tage. Preis inkl. MwSt.",
|
|
))
|
|
assert len(out) == 1
|
|
f = out[0]
|
|
assert f["check_id"] == "WIDERRUF-REACH-001"
|
|
assert f["severity"] == "HIGH"
|
|
assert f["b2c_scope"] == "b2c_strong"
|
|
assert "Art. 246a" in f["norm"]
|
|
assert "shop.de/widerruf" in f["evidence"]
|
|
|
|
def test_unreachable_plus_b2c_likely_medium(self):
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": True,
|
|
},
|
|
home_text="Shop bietet Wallbox und Tarif buchen.",
|
|
))
|
|
assert len(out) == 1
|
|
assert out[0]["severity"] == "MEDIUM"
|
|
assert out[0]["b2c_scope"] == "b2c_likely"
|
|
|
|
def test_unreachable_plus_b2b_only_no_finding(self):
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": True,
|
|
},
|
|
home_text="B2B only — kein Verkauf an Verbraucher.",
|
|
))
|
|
assert out == []
|
|
|
|
def test_unreachable_plus_unknown_scope_no_finding(self):
|
|
# Pure agency / B2B-services without clear shop signals — silent.
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": True,
|
|
},
|
|
home_text="Wir sind eine Beratungsagentur für Mittelstand.",
|
|
))
|
|
assert out == []
|
|
|
|
def test_discovery_not_attempted_no_finding(self):
|
|
# Avoid false positives when discovery had no homepage to crawl.
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": False,
|
|
},
|
|
home_text="In den Warenkorb. Preis inkl. MwSt.",
|
|
))
|
|
assert out == []
|
|
|
|
def test_footer_widerruf_link_suppresses_finding(self):
|
|
out = check_widerrufsbelehrung_reachability(_state(
|
|
widerruf_entry={
|
|
"url": "", "text": "",
|
|
"discovery_attempted": True,
|
|
},
|
|
home_text="Warenkorb. Lieferzeit. Preis inkl. MwSt.",
|
|
footer_text='<a href="/widerruf">Widerruf</a>',
|
|
))
|
|
assert out == []
|