Files
152-fz-scanner/tests/test_smoke.py
T
neo b9db935389 v2.0: исправлены критические баги, добавлены 4 новые проверки, 4-фазный cookie-аудит, retry-логика, YAML-словари, LLM-анализ, тесты
Исправлено:
- 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)
2026-06-10 00:49:07 +05:00

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