"""Tests for scanner/__main__.py — CLI commands, worst_level, file-lock.""" import json from pathlib import Path from tempfile import TemporaryDirectory from unittest.mock import MagicMock, patch import pytest from scanner.__main__ import _worst_level, _LEVEL_ORDER # --------------------------------------------------------------------------- # _worst_level # --------------------------------------------------------------------------- class TestWorstLevel: def test_all_green(self): assert _worst_level(["green", "green"]) == "green" def test_green_yellow(self): assert _worst_level(["green", "yellow"]) == "yellow" def test_green_red(self): assert _worst_level(["green", "red"]) == "red" def test_yellow_red(self): assert _worst_level(["yellow", "red"]) == "red" def test_red_yellow(self): assert _worst_level(["red", "yellow"]) == "red" def test_single_green(self): assert _worst_level(["green"]) == "green" def test_empty_returns_green(self): assert _worst_level([]) == "green" def test_many_levels(self): assert _worst_level(["green", "green", "yellow", "green", "red"]) == "red" class TestLevelOrder: def test_order_values(self): assert _LEVEL_ORDER["green"] == 0 assert _LEVEL_ORDER["yellow"] == 1 assert _LEVEL_ORDER["red"] == 2 def test_order_increasing(self): assert _LEVEL_ORDER["green"] < _LEVEL_ORDER["yellow"] < _LEVEL_ORDER["red"] # --------------------------------------------------------------------------- # cmd_scan file-lock # --------------------------------------------------------------------------- class TestCmdScanFileLock: @patch("scanner.__main__._crawl_and_extract") @patch("scanner.__main__.BaselineManager") def test_scan_without_data_dir_no_crash(self, mock_bm, mock_crawl): """cmd_scan ohne data_dir sollte keinen Lock versuchen.""" from scanner.__main__ import cmd_scan mock_crawl.return_value = ([], []) mock_bm_instance = MagicMock() mock_bm_instance.baseline_exists.return_value = False mock_bm.return_value = mock_bm_instance cfg = {"target": "https://test.de", "data_dir": None} args = MagicMock(config="test/config.yaml") result = cmd_scan(args, cfg) assert result == 0 @patch("scanner.__main__._crawl_and_extract") @patch("scanner.__main__.BaselineManager") def test_scan_with_data_dir_creates_lock(self, mock_bm, mock_crawl): """cmd_scan mit data_dir erstellt .scan.lock.""" from scanner.__main__ import cmd_scan import fcntl with TemporaryDirectory() as tmp: lock_path = Path(tmp) / ".scan.lock" mock_crawl.return_value = ([], []) mock_bm_instance = MagicMock() mock_bm_instance.baseline_exists.return_value = False mock_bm.return_value = mock_bm_instance cfg = {"target": "https://test.de", "data_dir": tmp} args = MagicMock(config="test/config.yaml") result = cmd_scan(args, cfg) assert result == 0 # Lock wurde erstellt und nach Scan wieder entfernt (oder leer) assert lock_path.exists() # --------------------------------------------------------------------------- # cmd_scan baseline_exists # --------------------------------------------------------------------------- class TestCmdScanNoBaseline: @patch("scanner.__main__._crawl_and_extract") @patch("scanner.__main__.BaselineManager") def test_no_baseline_returns_zero(self, mock_bm, mock_crawl): from scanner.__main__ import cmd_scan with TemporaryDirectory() as tmp: mock_crawl.return_value = ([{"url": "https://test.de/"}], []) mock_bm_instance = MagicMock() mock_bm_instance.baseline_exists.return_value = False mock_bm.return_value = mock_bm_instance cfg = {"target": "https://test.de", "data_dir": tmp} args = MagicMock(config="test/config.yaml") result = cmd_scan(args, cfg) assert result == 0 # --------------------------------------------------------------------------- # _crawl_and_extract # --------------------------------------------------------------------------- class TestCrawlAndExtract: @patch("scanner.__main__.Crawler") def test_empty_crawl(self, mock_crawler_cls): from scanner.__main__ import _crawl_and_extract mock_crawler = MagicMock() mock_crawler.crawl.return_value = { "target": "https://test.de", "pages": [], "errors": [{"url": "https://test.de/", "error": "timeout"}], } mock_crawler_cls.return_value = mock_crawler cfg = { "target": "https://test.de", "crawl": {"max_pages": 10, "delay_seconds": 0.1, "skip_extensions": [], "exclude_paths": [], "include_paths": [], "sitemap": False}, "request_timeout": 10, "user_agent": "test", } pages, errors = _crawl_and_extract(cfg) assert len(pages) == 0 assert len(errors) == 1 @patch("scanner.__main__.Crawler") def test_crawl_with_pages(self, mock_crawler_cls): from scanner.__main__ import _crawl_and_extract mock_crawler = MagicMock() mock_crawler.crawl.return_value = { "target": "https://test.de", "pages": [ {"url": "https://test.de/", "status": 200, "html": "Test", "headers": {}}, ], "errors": [], } mock_crawler_cls.return_value = mock_crawler cfg = { "target": "https://test.de", "crawl": {"max_pages": 10, "delay_seconds": 0.1, "skip_extensions": [], "exclude_paths": [], "include_paths": [], "sitemap": False}, "request_timeout": 10, "user_agent": "test", } pages, errors = _crawl_and_extract(cfg) assert len(pages) == 1 assert len(errors) == 0 assert "text" in pages[0] # extract_page wurde aufgerufen # --------------------------------------------------------------------------- # _run_content_check # --------------------------------------------------------------------------- class TestRunContentCheck: def test_empty_baseline_and_snapshot(self): from scanner.__main__ import _run_content_check cfg = { "target": "https://test.de", "config_dir": "/tmp/test/config", "security_headers": [], } snap = {"pages": {}, "errors": []} baseline = {"manifest": {}, "pages": {}} diff, assessment, wl_violations, missing_sec = _run_content_check(cfg, snap, baseline) assert assessment["level"] == "green" assert assessment["score"] == 0 # --------------------------------------------------------------------------- # Score aggregation in cmd_scan # --------------------------------------------------------------------------- class TestScoreAggregation: @patch("scanner.__main__._crawl_and_extract") @patch("scanner.__main__.BaselineManager") @patch("scanner.__main__._run_content_check") @patch("scanner.__main__._build_report") @patch("scanner.__main__._write_report") @patch("scanner.__main__._print_summary") @patch("scanner.__main__.send_alert") def test_scan_aggregates_levels(self, mock_alert, mock_print, mock_write, mock_build, mock_check, mock_bm, mock_crawl): from scanner.__main__ import cmd_scan with TemporaryDirectory() as tmp: mock_crawl.return_value = ([{"url": "https://test.de/"}], []) mock_bm_instance = MagicMock() mock_bm_instance.baseline_exists.return_value = True mock_bm_instance.load_baseline.return_value = {"manifest": {}, "pages": {}} mock_bm.return_value = mock_bm_instance # content check: yellow mock_check.return_value = ( {"new_external_domains": [], "page_diffs": []}, {"level": "yellow", "score": 30, "reasons": ["Test"], "exit_code": 1}, [], {} ) cfg = { "target": "https://test.de", "data_dir": tmp, "reports_dir": f"{tmp}/reports", "ai_analysis": {}, "periodic_checks": {"enabled": False}, } args = MagicMock(config="test/config.yaml") result = cmd_scan(args, cfg) assert result == 1 # exit_code für yellow