cb4b352846
Nimmt einen kompletten Site-Walk als WebKit-Browser-Session
inkl. Video auf. Reviewer kann nachträglich exakt nachvollziehen,
wie die Engine zum Befund kam.
consent-tester:
- services/audit_walk_recorder.py: Playwright record_video_dir,
iPhone-Viewport-free 1280×800. Goto homepage → Banner-Accept
(Best-Effort: 12 Text-Phrasen + 5 CMP-Fallback-Selektoren) →
Footer-Links sammeln (compliance-relevant gefiltert) →
pro Link navigate + Dwell-Time → JSON-Action-Index mit
UTC-Timestamps + SHA-256 vom Video als Manipulation-Schutz.
- routes_audit_walk.py: POST /scan-audit-walk; statische
Serves für /audit-walks/{walk_id}/video.webm + walk.json.
- main.py: Router registriert.
backend:
- _b17_wiring.py: Triggert /scan-audit-walk, speichert
Walk-Metadata in state["audit_walk"]. Render-Block mit
HTML-Tabelle aller Actions (HH:MM:SS + Aktion + Detail) +
Links zu Video und walk.json.
- _orchestrator.py: run_b17 nach run_b16, async-aufgerufen.
- mail_render_v2/_compose.py: audit_walk_html im V2-Layout.
- test_b17_audit_walk.py: 8 Tests (Render-Pfade + Wiring).
Stufe-2 (Akkordeon-Expansion) und Stufe-3 (DSMS-CID-Anchor)
folgen separat.
Real-World-Smoke gegen Elli:
- 581 KB Video, SHA-256 verifizierbar
- 3 Footer-Links besucht (Impressum, Datenschutzerkl., Nutzungs-)
- 6 Actions im JSON-Index
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Tests for B17 Audit-Walk-Wiring (Stufe 1)."""
|
|
|
|
import asyncio
|
|
from unittest.mock import patch, MagicMock, AsyncMock
|
|
|
|
import pytest
|
|
|
|
from compliance.api.agent_check._b17_wiring import _render, run_b17
|
|
|
|
|
|
_FAKE_WALK = {
|
|
"walk_id": "abc123def456",
|
|
"url": "https://example.com/",
|
|
"started_at": "2026-06-07T10:00:00+00:00",
|
|
"completed_at": "2026-06-07T10:00:30+00:00",
|
|
"engine": "playwright/webkit",
|
|
"viewport": "1280x800",
|
|
"actions": [
|
|
{"timestamp": "2026-06-07T10:00:00+00:00", "action": "goto",
|
|
"url": "https://example.com/", "status": 200},
|
|
{"timestamp": "2026-06-07T10:00:02+00:00", "action": "accept_banner",
|
|
"result": "clicked", "phrase": "alle akzeptieren"},
|
|
{"timestamp": "2026-06-07T10:00:04+00:00",
|
|
"action": "discover_footer_links", "count": 3, "links": []},
|
|
{"timestamp": "2026-06-07T10:00:06+00:00", "action": "navigate",
|
|
"url": "https://example.com/datenschutz",
|
|
"anchor_text": "Datenschutz", "status": 200,
|
|
"title": "Datenschutzerklärung"},
|
|
],
|
|
"video": {
|
|
"filename": "video.webm",
|
|
"size_bytes": 512000,
|
|
"sha256": "a1b2c3d4e5f67890fedcba0987654321ffffeeeeddddccccbbbbaaaa00001111",
|
|
},
|
|
}
|
|
|
|
|
|
class TestRender:
|
|
def test_renders_walk_id_and_link(self):
|
|
html = _render(_FAKE_WALK)
|
|
assert "abc123def456" in html
|
|
assert "video.webm" in html
|
|
assert "walk.json" in html
|
|
|
|
def test_includes_sha_prefix(self):
|
|
html = _render(_FAKE_WALK)
|
|
# First 12 chars of sha
|
|
assert "a1b2c3d4e5f6" in html
|
|
|
|
def test_action_table_lists_all_actions(self):
|
|
html = _render(_FAKE_WALK)
|
|
# All four actions appear as <tr>
|
|
assert html.count("<tr>") >= 4 # incl. header
|
|
|
|
def test_nav_count_reflects_navigate_actions(self):
|
|
html = _render(_FAKE_WALK)
|
|
# 1 navigate in the fixture
|
|
assert "1 Compliance-Seiten" in html
|
|
|
|
|
|
class TestRunB17:
|
|
def test_no_request_skipped(self):
|
|
state = {}
|
|
asyncio.run(run_b17(state))
|
|
assert "audit_walk" not in state
|
|
|
|
def test_no_url_skipped(self):
|
|
state = {"req": MagicMock(documents=[MagicMock(url="")])}
|
|
asyncio.run(run_b17(state))
|
|
assert "audit_walk" not in state
|
|
|
|
def test_consent_tester_failure_skipped(self):
|
|
req = MagicMock(documents=[MagicMock(url="https://example.com/dse")])
|
|
state = {"req": req}
|
|
with patch(
|
|
"compliance.api.agent_check._b17_wiring.httpx.AsyncClient"
|
|
) as mock_client:
|
|
instance = mock_client.return_value.__aenter__.return_value
|
|
instance.post = AsyncMock(side_effect=Exception("nope"))
|
|
asyncio.run(run_b17(state))
|
|
assert "audit_walk" not in state
|
|
|
|
def test_success_populates_state(self):
|
|
req = MagicMock(documents=[MagicMock(url="https://example.com/dse")])
|
|
state = {"req": req}
|
|
resp = MagicMock(status_code=200)
|
|
resp.json = MagicMock(return_value=_FAKE_WALK)
|
|
with patch(
|
|
"compliance.api.agent_check._b17_wiring.httpx.AsyncClient"
|
|
) as mock_client:
|
|
instance = mock_client.return_value.__aenter__.return_value
|
|
instance.post = AsyncMock(return_value=resp)
|
|
asyncio.run(run_b17(state))
|
|
assert state["audit_walk"]["walk_id"] == "abc123def456"
|
|
assert "video.webm" in state["audit_walk_html"]
|