"""Tests for asset hashing and comparison.""" from unittest.mock import MagicMock, patch import pytest import requests from scanner.baseline import BaselineManager from scanner.checker import fetch_asset_hashes from scanner.differ import compare_asset_hashes, score_asset_diff CFG = {"scoring": {}, "thresholds": {}} # --------------------------------------------------------------------------- # fetch_asset_hashes # --------------------------------------------------------------------------- class TestFetchAssetHashes: def _mock_session(self, status=200, content=b"data"): resp = MagicMock() resp.status_code = status resp.iter_content.return_value = [content] resp.close = MagicMock() inst = MagicMock() inst.get.return_value = resp inst.close = MagicMock() return inst def test_successful_fetch_returns_sha256(self): import hashlib data = b"hello asset" expected = hashlib.sha256(data).hexdigest() with patch("scanner.checker.requests.Session") as MockSession: MockSession.return_value = self._mock_session(content=data) results = fetch_asset_hashes(["https://x.de/app.js"], delay=0) assert results["https://x.de/app.js"]["sha256"] == expected assert results["https://x.de/app.js"]["error"] is None def test_http_error_recorded(self): with patch("scanner.checker.requests.Session") as MockSession: MockSession.return_value = self._mock_session(status=404) results = fetch_asset_hashes(["https://x.de/missing.js"], delay=0) assert results["https://x.de/missing.js"]["sha256"] is None assert "HTTP 404" in results["https://x.de/missing.js"]["error"] def test_connection_error_recorded(self): with patch("scanner.checker.requests.Session") as MockSession: inst = MockSession.return_value inst.get.side_effect = requests.exceptions.ConnectionError("refused") inst.close = MagicMock() results = fetch_asset_hashes(["https://unreachable.de/x.js"], delay=0) assert results["https://unreachable.de/x.js"]["sha256"] is None assert results["https://unreachable.de/x.js"]["error"] is not None def test_empty_list_returns_empty(self): with patch("scanner.checker.requests.Session"): results = fetch_asset_hashes([], delay=0) assert results == {} # --------------------------------------------------------------------------- # compare_asset_hashes # --------------------------------------------------------------------------- class TestCompareAssetHashes: def _h(self, sha, type_="img", size=100): return {"sha256": sha, "size": size, "type": type_, "error": None} def test_no_changes_empty_diff(self): h = {"https://x.de/img.jpg": self._h("aaa")} result = compare_asset_hashes(h, h) assert result["changed"] == [] assert result["new"] == [] assert result["missing"] == [] def test_changed_hash_detected(self): baseline = {"https://x.de/img.jpg": self._h("aaa111")} current = {"https://x.de/img.jpg": self._h("bbb222")} result = compare_asset_hashes(baseline, current) assert len(result["changed"]) == 1 assert result["changed"][0]["url"] == "https://x.de/img.jpg" def test_new_asset_detected(self): baseline = {} current = {"https://x.de/new.jpg": self._h("abc")} result = compare_asset_hashes(baseline, current) assert "https://x.de/new.jpg" in result["new"] def test_missing_asset_detected(self): baseline = {"https://x.de/old.jpg": self._h("abc")} current = {} result = compare_asset_hashes(baseline, current) assert "https://x.de/old.jpg" in result["missing"] def test_fetch_error_not_in_changed(self): baseline = {"https://x.de/img.jpg": self._h("aaa")} current = {"https://x.de/img.jpg": {"sha256": None, "size": None, "type": "img", "error": "timeout"}} result = compare_asset_hashes(baseline, current) assert result["changed"] == [] assert len(result["fetch_errors"]) == 1 # --------------------------------------------------------------------------- # score_asset_diff # --------------------------------------------------------------------------- class TestScoreAssetDiff: def test_no_changes_green(self): result = score_asset_diff({"changed": [], "new": [], "missing": [], "fetch_errors": [], "total_checked": 5}, CFG) assert result["level"] == "green" assert result["score"] == 0 def test_changed_script_scores_40(self): diff = {"changed": [{"url": "https://x.de/app.js", "type": "script", "old_sha256": "a…", "new_sha256": "b…", "size_diff": 0}], "new": [], "missing": [], "fetch_errors": [], "total_checked": 1} result = score_asset_diff(diff, CFG) assert result["score"] == 40 assert result["level"] == "yellow" def test_changed_stylesheet_scores_30(self): diff = {"changed": [{"url": "https://x.de/style.css", "type": "link_rel", "old_sha256": "a…", "new_sha256": "b…", "size_diff": 0}], "new": [], "missing": [], "fetch_errors": [], "total_checked": 1} result = score_asset_diff(diff, CFG) assert result["score"] == 30 assert result["level"] == "yellow" def test_changed_image_scores_20(self): diff = {"changed": [{"url": "https://x.de/logo.png", "type": "img", "old_sha256": "a…", "new_sha256": "b…", "size_diff": 100}], "new": [], "missing": [], "fetch_errors": [], "total_checked": 1} result = score_asset_diff(diff, CFG) assert result["score"] == 20 assert result["level"] == "yellow" def test_two_changed_scripts_red(self): entry = {"url": "https://x.de/x.js", "type": "script", "old_sha256": "a…", "new_sha256": "b…", "size_diff": 0} diff = {"changed": [entry, {**entry, "url": "https://x.de/y.js"}], "new": [], "missing": [], "fetch_errors": [], "total_checked": 2} result = score_asset_diff(diff, CFG) assert result["score"] == 80 assert result["level"] == "red" assert result["exit_code"] == 2 class TestAutoAssetBaselineOnApprove: """approve --all soll die Datei-Baseline automatisch mit anlegen.""" def test_establish_asset_baseline_saves_hashes(self, tmp_path): from scanner.__main__ import _establish_asset_baseline bm = BaselineManager(tmp_path) page = { "url": "https://x.de/", "status": 200, "links": {"script": [{"url": "https://x.de/app.js", "class": "internal"}], "link_rel": [], "img": []}, } snap_dir = bm.save_snapshot([page]) with patch("scanner.__main__.fetch_asset_hashes") as mock_fetch: mock_fetch.return_value = { "https://x.de/app.js": {"sha256": "abc123", "size": 10, "error": None} } _establish_asset_baseline({"request_timeout": 15}, bm, snap_dir) saved = bm.load_asset_hashes() assert "https://x.de/app.js" in saved assert saved["https://x.de/app.js"]["sha256"] == "abc123" def test_no_assets_writes_nothing(self, tmp_path): from scanner.__main__ import _establish_asset_baseline bm = BaselineManager(tmp_path) page = {"url": "https://x.de/", "status": 200, "links": {"script": [], "link_rel": [], "img": []}} snap_dir = bm.save_snapshot([page]) with patch("scanner.__main__.fetch_asset_hashes") as mock_fetch: _establish_asset_baseline({"request_timeout": 15}, bm, snap_dir) mock_fetch.assert_not_called() assert bm.load_asset_hashes() == {}