All checks were successful
CI / changes (push) Successful in 8s
CI / changes (pull_request) Successful in 6s
CI / frontend-tests (push) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / backend-tests (push) Successful in 6m26s
CI / backend-tests (pull_request) Successful in 6m26s
171 lines
6.2 KiB
Python
171 lines
6.2 KiB
Python
"""Тесты pravo_gov66_client — offline, без живой сети.
|
||
|
||
Покрывают: парсинг search-HTML (doc_id/title/url), парсинг страницы документа
|
||
(PDF-ссылка), fallback на ecp-вариант, отсутствие PDF.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from app.services.scrapers.pravo_gov66_client import (
|
||
_LAW_TYPE_POSTANOVLENIE,
|
||
_extract_pdf_url,
|
||
_parse_search_html,
|
||
)
|
||
|
||
# ── Фикстура HTML страницы поиска ─────────────────────────────────────────────
|
||
# Реальный фрагмент страницы https://www.pravo.gov66.ru/search/?q=изъятие+земельных+участков
|
||
# Содержит ссылки вида /NNNNN/ с заголовком постановления.
|
||
_SEARCH_HTML_FIXTURE = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><title>Поиск</title></head>
|
||
<body>
|
||
<div class="search-results">
|
||
<ul>
|
||
<li class="result-item">
|
||
<a href="/35496/">
|
||
Об изъятии земельных участков для государственных нужд
|
||
Свердловской области в целях реконструкции ул. Татищева
|
||
в г. Екатеринбурге
|
||
</a>
|
||
</li>
|
||
<li class="result-item">
|
||
<a href="/34812/">
|
||
О резервировании земельных участков для нужд Екатеринбург
|
||
</a>
|
||
</li>
|
||
<li class="result-item">
|
||
<!-- Ссылка на категорию, не документ — должна игнорироваться -->
|
||
<a href="/category/law/">Категория</a>
|
||
</li>
|
||
<li class="result-item">
|
||
<!-- Дубль doc_id 35496 — должен дедуплицироваться -->
|
||
<a href="/35496/">Дубль</a>
|
||
</li>
|
||
<li class="result-item">
|
||
<!-- Нечисловой id — не должен попасть -->
|
||
<a href="/some-slug/">Текстовый slug</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# ── Фикстура HTML страницы документа с обычным PDF ────────────────────────────
|
||
_DOC_HTML_WITH_PDF = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<body>
|
||
<h1>Постановление № 509-ПП</h1>
|
||
<p>
|
||
Скачать документ:
|
||
<a href="/media/pravo/509-ПП_ab1234cd.pdf">Скачать PDF</a>
|
||
</p>
|
||
<p>
|
||
Подписанная версия:
|
||
<a href="/media/pravo/ecp/509-ПП.pdf">Скачать подписанный PDF</a>
|
||
</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# ── Фикстура HTML страницы документа только с ecp PDF ─────────────────────────
|
||
_DOC_HTML_ECP_ONLY = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<body>
|
||
<h1>Постановление № 312-ПП</h1>
|
||
<p>
|
||
<a href="/media/pravo/ecp/312-ПП.pdf">Подписанный PDF</a>
|
||
</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
# ── Фикстура HTML страницы документа без PDF ──────────────────────────────────
|
||
_DOC_HTML_NO_PDF = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<body>
|
||
<h1>Постановление без PDF</h1>
|
||
<p>Текст постановления размещён ниже.</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
|
||
class TestLawTypeConstant:
|
||
def test_postanovlenie_is_numeric_string(self) -> None:
|
||
"""Числовой код — сервер игнорирует текстовые значения типа «Постановление»."""
|
||
assert _LAW_TYPE_POSTANOVLENIE == "1"
|
||
assert _LAW_TYPE_POSTANOVLENIE.isdigit()
|
||
|
||
|
||
class TestParseSearchHtml:
|
||
def test_extracts_doc_ids_and_titles(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
ids = [r["doc_id"] for r in results]
|
||
assert "35496" in ids
|
||
assert "34812" in ids
|
||
|
||
def test_ignores_non_numeric_paths(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
ids = [r["doc_id"] for r in results]
|
||
assert "some-slug" not in ids
|
||
|
||
def test_deduplicates_doc_ids(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
ids = [r["doc_id"] for r in results]
|
||
assert ids.count("35496") == 1
|
||
|
||
def test_ignores_category_links(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
# /category/law/ не числовой id — не попадает.
|
||
ids = [r["doc_id"] for r in results]
|
||
assert len(ids) == 2
|
||
|
||
def test_url_is_absolute(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
for r in results:
|
||
assert r["url"].startswith("https://www.pravo.gov66.ru/")
|
||
|
||
def test_title_extracted(self) -> None:
|
||
results = _parse_search_html(_SEARCH_HTML_FIXTURE)
|
||
doc = next(r for r in results if r["doc_id"] == "35496")
|
||
assert "изъятии" in doc["title"].lower()
|
||
|
||
def test_empty_html_returns_empty(self) -> None:
|
||
assert _parse_search_html("") == []
|
||
|
||
def test_html_no_doc_links_returns_empty(self) -> None:
|
||
html = "<html><body><a href='/category/'>Cat</a></body></html>"
|
||
assert _parse_search_html(html) == []
|
||
|
||
|
||
class TestExtractPdfUrl:
|
||
def test_prefers_non_ecp_pdf(self) -> None:
|
||
url = _extract_pdf_url(_DOC_HTML_WITH_PDF)
|
||
assert url is not None
|
||
assert "ecp" not in url
|
||
assert url.endswith(".pdf")
|
||
|
||
def test_non_ecp_pdf_is_absolute(self) -> None:
|
||
url = _extract_pdf_url(_DOC_HTML_WITH_PDF)
|
||
assert url is not None
|
||
assert url.startswith("https://www.pravo.gov66.ru/media/pravo/")
|
||
|
||
def test_falls_back_to_ecp_when_no_regular_pdf(self) -> None:
|
||
url = _extract_pdf_url(_DOC_HTML_ECP_ONLY)
|
||
assert url is not None
|
||
assert "ecp" in url
|
||
assert url.endswith(".pdf")
|
||
|
||
def test_returns_none_when_no_pdf(self) -> None:
|
||
url = _extract_pdf_url(_DOC_HTML_NO_PDF)
|
||
assert url is None
|
||
|
||
def test_ecp_fallback_is_absolute(self) -> None:
|
||
url = _extract_pdf_url(_DOC_HTML_ECP_ONLY)
|
||
assert url is not None
|
||
assert url.startswith("https://www.pravo.gov66.ru/")
|