Исправлено: - engine: _calculate_score() при ошибке загрузки (score не 100 для мёртвых сайтов) - privacy_link: замена page.goto() на aiohttp (гонка данных) - form_checkbox: 3-уровневый поиск label + dark-pattern detection - server_location: синхронный urllib → aiohttp - api/server: перепутанные request/response модели в batch - main.py: csv --checks флаг - engine: фазовая модель (cookie_audit отдельно, остальные параллельно) - engine: retry-логика (3 попытки, exponential backoff) - PDF: emoji заменены на текст (WeasyPrint) Добавлено: - consent_doc: проверка отдельного согласия на обработку ПД - operator_info: проверка ИНН/ОГРН/DPO (читает главную + политику) - cookie_audit: 4-фазный аудит (load→detect→reject→compare) - llm_policy: опциональный LLM-анализ 16 разделов (--deep) - external_scripts: YAML-словари trackers.yaml (critical/warning/russian) - logging (output.py), .gitignore, pyproject.toml - smoke-тесты (16/16)
139 lines
6.0 KiB
Python
139 lines
6.0 KiB
Python
"""
|
|
Smoke-тест: проверяет, что все 7 быстрых проверок импортируются,
|
|
создаются и запускаются на минимальной HTML-странице без ошибок.
|
|
"""
|
|
import pytest
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from scanner.checks import ALL_CHECKS, QUICK_CHECKS
|
|
from scanner.models import CheckStatus
|
|
|
|
# Минимальная HTML-страница для тестирования
|
|
MOCK_HTML = """<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Test Site</title></head>
|
|
<body>
|
|
<footer>
|
|
<a href="/privacy">Политика конфиденциальности</a>
|
|
<a href="/consent">Согласие на обработку персональных данных</a>
|
|
</footer>
|
|
<form action="/submit" method="POST">
|
|
<input type="text" name="name" placeholder="Имя">
|
|
<input type="email" name="email" placeholder="Email">
|
|
<label for="agree">
|
|
<input type="checkbox" id="agree" name="agree">
|
|
Я даю согласие на обработку персональных данных
|
|
</label>
|
|
<button type="submit">Отправить</button>
|
|
</form>
|
|
<div id="cookie-banner" class="cookie-consent">
|
|
<p>Мы используем cookies</p>
|
|
<button id="accept-cookies">Принять</button>
|
|
<button id="reject-cookies">Отказаться</button>
|
|
</div>
|
|
<script src="https://mc.yandex.ru/metrika/tag.js"></script>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
class TestAllChecksImport:
|
|
"""Проверка что все проверки импортируются."""
|
|
|
|
def test_quick_checks_count(self):
|
|
assert len(QUICK_CHECKS) == 7, f"Expected 7 quick checks, got {len(QUICK_CHECKS)}"
|
|
|
|
def test_all_checks_registered(self):
|
|
for name in QUICK_CHECKS:
|
|
assert name in ALL_CHECKS, f"Check '{name}' not in ALL_CHECKS"
|
|
|
|
@pytest.mark.parametrize("check_name", QUICK_CHECKS)
|
|
def test_check_instantiable(self, check_name):
|
|
"""Каждая проверка создаётся без ошибок."""
|
|
check_cls = ALL_CHECKS[check_name]
|
|
instance = check_cls()
|
|
assert instance.name == check_name
|
|
assert instance.title
|
|
|
|
|
|
class TestCheckLogic:
|
|
"""Проверка логики проверок (без Playwright — unit tests)."""
|
|
|
|
def test_form_checkbox_patterns(self):
|
|
from scanner.checks.form_checkbox import FormCheckboxCheck
|
|
import re
|
|
check = FormCheckboxCheck()
|
|
# Паттерны должны матчить русский текст согласия
|
|
assert any(re.search(p, "согласие на обработку данных", re.IGNORECASE) for p in check.CONSENT_PATTERNS)
|
|
assert any(re.search(p, "я согласен на обработку персональных данных", re.IGNORECASE) for p in check.CONSENT_PATTERNS)
|
|
|
|
def test_operator_info_inn_pattern(self):
|
|
from scanner.checks.operator_info import OperatorInfoCheck
|
|
import re
|
|
check = OperatorInfoCheck()
|
|
m = re.search(check.INN_PATTERN, "ИНН: 0278901234", re.IGNORECASE)
|
|
assert m, "INN pattern should match 10-digit INN"
|
|
assert m.group(1) == "0278901234"
|
|
|
|
def test_external_scripts_yaml_loads(self):
|
|
from scanner.checks.external_scripts import ExternalScriptsCheck
|
|
check = ExternalScriptsCheck()
|
|
trackers = check._load_trackers()
|
|
assert "critical" in trackers
|
|
assert "russian" in trackers
|
|
assert any(e["domain"] == "google-analytics.com" for e in trackers["critical"])
|
|
|
|
|
|
class TestScoring:
|
|
"""Проверка логики скоринга."""
|
|
|
|
def test_score_calculation_critical(self):
|
|
from scanner.models import SiteScan, CheckResult, CheckStatus, RiskLevel
|
|
from datetime import datetime
|
|
scan = SiteScan(url="https://test.ru", scanned_at=datetime.now())
|
|
scan.checks = [
|
|
CheckResult(name="t1", title="Test 1", status=CheckStatus.FAIL, risk_level=RiskLevel.CRITICAL),
|
|
CheckResult(name="t2", title="Test 2", status=CheckStatus.PASS, risk_level=RiskLevel.LOW),
|
|
]
|
|
scan._calculate_score()
|
|
assert scan.score == 70 # 100 - 30
|
|
assert scan.is_compliant # score=70 — на границе, но compliant
|
|
|
|
def test_score_all_pass(self):
|
|
from scanner.models import SiteScan, CheckResult, CheckStatus, RiskLevel
|
|
from datetime import datetime
|
|
scan = SiteScan(url="https://test.ru", scanned_at=datetime.now())
|
|
scan.checks = [
|
|
CheckResult(name="t1", title="Test 1", status=CheckStatus.PASS, risk_level=RiskLevel.HIGH),
|
|
]
|
|
scan._calculate_score()
|
|
assert scan.score == 100
|
|
assert scan.is_compliant
|
|
|
|
def test_score_multiple_critical(self):
|
|
from scanner.models import SiteScan, CheckResult, CheckStatus, RiskLevel
|
|
from datetime import datetime
|
|
scan = SiteScan(url="https://test.ru", scanned_at=datetime.now())
|
|
scan.checks = [
|
|
CheckResult(name="t1", title="C1", status=CheckStatus.FAIL, risk_level=RiskLevel.CRITICAL),
|
|
CheckResult(name="t2", title="C2", status=CheckStatus.FAIL, risk_level=RiskLevel.CRITICAL),
|
|
CheckResult(name="t3", title="H1", status=CheckStatus.FAIL, risk_level=RiskLevel.HIGH),
|
|
]
|
|
scan._calculate_score()
|
|
assert scan.score == 20 # 100 - 30 - 30 - 20
|
|
assert not scan.is_compliant
|
|
|
|
|
|
class TestEnginePhases:
|
|
"""Проверка фазовой модели в engine."""
|
|
|
|
def test_mutating_checks_identified(self):
|
|
"""cookie_audit должен быть в mutating_checks."""
|
|
from scanner.checks import QUICK_CHECKS
|
|
mutating = {"cookie_audit"}
|
|
assert "cookie_audit" in QUICK_CHECKS, "cookie_audit должен быть в QUICK_CHECKS"
|
|
non_mutating = [n for n in QUICK_CHECKS if n not in mutating]
|
|
assert len(non_mutating) == 6 # 7 quick - 1 mutating
|