354 lines
14 KiB
Python
354 lines
14 KiB
Python
from __future__ import annotations
|
||
|
||
from apps.parsers.clients.base import HTTPClientError
|
||
from apps.parsers.clients.common.structured import (
|
||
EIS_CA_BUNDLE_PATH,
|
||
StructuredDataClient,
|
||
)
|
||
from django.test import SimpleTestCase
|
||
|
||
|
||
class _FakeHTTPClient:
|
||
def __init__(self, responses: dict[str, bytes | Exception]) -> None:
|
||
self.responses = responses
|
||
self.downloaded_urls: list[str] = []
|
||
|
||
def download_file(self, endpoint: str, **_kwargs) -> bytes:
|
||
self.downloaded_urls.append(endpoint)
|
||
response = self.responses[endpoint]
|
||
if isinstance(response, Exception):
|
||
raise response
|
||
return response
|
||
|
||
|
||
class _FakeGispHTTPClient:
|
||
def __init__(self) -> None:
|
||
self.endpoint = ""
|
||
self.payload: dict | None = None
|
||
|
||
def post_json(
|
||
self,
|
||
endpoint: str,
|
||
*,
|
||
payload: dict | None = None,
|
||
**_kwargs,
|
||
) -> dict:
|
||
self.endpoint = endpoint
|
||
self.payload = payload
|
||
return {"items": [{"res_number": "123", "_product_name": "Станок"}]}
|
||
|
||
|
||
class StructuredDataClientGispTest(SimpleTestCase):
|
||
def test_products_request_omits_unsupported_res_date_sort(self):
|
||
client = StructuredDataClient(source="mpt_products")
|
||
fake_http_client = _FakeGispHTTPClient()
|
||
client._http_client = fake_http_client
|
||
|
||
records = client.fetch_records(file_url="https://gisp.gov.ru/pp719v2/pub/prod/")
|
||
|
||
self.assertEqual(len(records), 1)
|
||
self.assertEqual(records[0].external_id, "123")
|
||
self.assertEqual(
|
||
fake_http_client.endpoint,
|
||
"https://gisp.gov.ru/pp719v2/pub/prod/b/",
|
||
)
|
||
self.assertEqual(
|
||
fake_http_client.payload,
|
||
{
|
||
"opt": {
|
||
"skip": 0,
|
||
"take": 100,
|
||
"requireTotalCount": True,
|
||
}
|
||
},
|
||
)
|
||
|
||
|
||
class StructuredDataClientFasGozParsingTest(SimpleTestCase):
|
||
def test_person_name_populates_record_title(self):
|
||
client = StructuredDataClient(source="fas_goz")
|
||
|
||
records = client.fetch_records(
|
||
content="""
|
||
<table>
|
||
<tr>
|
||
<td>РГОЗ-001</td>
|
||
<td>ФАС России</td>
|
||
<td>Постановление № 1</td>
|
||
<td>14.07.2026</td>
|
||
<td>Исполнено</td>
|
||
<td>ООО ПРОГРЕСС</td>
|
||
<td>ПРОГРЕСС</td>
|
||
<td>г. Москва</td>
|
||
<td>7707083893</td>
|
||
</tr>
|
||
</table>
|
||
""".encode(),
|
||
file_name="register.html",
|
||
)
|
||
|
||
self.assertEqual(len(records), 1)
|
||
self.assertEqual(records[0].organisation_name, "ООО ПРОГРЕСС")
|
||
self.assertEqual(records[0].title, "ООО ПРОГРЕСС")
|
||
|
||
|
||
class StructuredDataClientEisCardEnrichmentTest(SimpleTestCase):
|
||
def test_eis_client_uses_project_scoped_trusted_ca_bundle(self):
|
||
client = StructuredDataClient(source="procurements_44fz")
|
||
|
||
self.assertTrue(EIS_CA_BUNDLE_PATH.is_file())
|
||
self.assertEqual(client.http_client.verify_ssl, str(EIS_CA_BUNDLE_PATH))
|
||
|
||
def test_procurement_card_enriches_customer_identity_from_detail_page(self):
|
||
detail_url = (
|
||
"https://zakupki.gov.ru/epz/order/notice/ea20/view/"
|
||
"common-info.html?regNumber=0338100002026000022"
|
||
)
|
||
client = StructuredDataClient(source="procurements_44fz")
|
||
client._http_client = _FakeHTTPClient(
|
||
{
|
||
detail_url: """
|
||
<html>
|
||
<body>
|
||
<h2>Сведения о заказчике</h2>
|
||
<div>Полное наименование</div>
|
||
<div>ФЕДЕРАЛЬНОЕ ГБУ НАУКИ</div>
|
||
<div>ИНН / КПП</div>
|
||
<div>4101020011 / 410101001</div>
|
||
<div>ОГРН</div>
|
||
<div>1024101023456</div>
|
||
</body>
|
||
</html>
|
||
""".encode()
|
||
}
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<html>
|
||
<body>
|
||
<div class="search-registry-entry-block">
|
||
<a href="{detail_url}">№ 0338100002026000022</a>
|
||
<div>Работа комиссии</div>
|
||
<div>Объект закупки</div>
|
||
<div>Поставка оборудования</div>
|
||
<div>Заказчик</div>
|
||
<div>ФЕДЕРАЛЬНОЕ ГБУ НАУКИ</div>
|
||
<div>Начальная цена</div>
|
||
<div>1 000,00 ₽</div>
|
||
<div>Размещено</div>
|
||
<div>19.05.2026</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(len(records), 1)
|
||
record = records[0]
|
||
self.assertEqual(record.inn, "4101020011")
|
||
self.assertEqual(record.ogrn, "1024101023456")
|
||
self.assertEqual(record.payload["kpp"], "410101001")
|
||
self.assertEqual(record.payload["detail_url"], detail_url)
|
||
self.assertEqual(client._http_client.downloaded_urls, [detail_url])
|
||
|
||
def test_procurement_card_keeps_record_when_detail_page_is_unavailable(self):
|
||
detail_url = (
|
||
"https://zakupki.gov.ru/epz/order/notice/ea20/view/"
|
||
"common-info.html?regNumber=0338100002026000022"
|
||
)
|
||
client = StructuredDataClient(source="procurements_44fz")
|
||
client._http_client = _FakeHTTPClient(
|
||
{detail_url: HTTPClientError("detail unavailable", url=detail_url)}
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<html>
|
||
<body>
|
||
<div class="search-registry-entry-block">
|
||
<a href="{detail_url}">№ 0338100002026000022</a>
|
||
<div>Объект закупки</div>
|
||
<div>Поставка оборудования</div>
|
||
<div>Заказчик</div>
|
||
<div>ФЕДЕРАЛЬНОЕ ГБУ НАУКИ</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(len(records), 1)
|
||
self.assertEqual(records[0].inn, "")
|
||
self.assertEqual(records[0].ogrn, "")
|
||
self.assertEqual(records[0].organisation_name, "ФЕДЕРАЛЬНОЕ ГБУ НАУКИ")
|
||
|
||
def test_procurement_card_prefers_organization_page_for_identity(self):
|
||
detail_url = (
|
||
"https://zakupki.gov.ru/epz/order/notice/zk20/view/"
|
||
"common-info.html?regNumber=0322300001726000667"
|
||
)
|
||
identity_url = (
|
||
"https://zakupki.gov.ru/epz/organization/view/"
|
||
"info.html?organizationCode=03223000017"
|
||
)
|
||
client = StructuredDataClient(source="procurements_44fz")
|
||
client._http_client = _FakeHTTPClient(
|
||
{
|
||
identity_url: """
|
||
<div>Заказчик</div>
|
||
<div>21.07.2016</div>
|
||
<div>ОГРН</div><div>1022701405737</div>
|
||
<div>ИНН</div><div>2725006476</div>
|
||
<div>КПП</div><div>272501001</div>
|
||
<div>Полное наименование</div><div>КГБУЗ БОЛЬНИЦА</div>
|
||
""".encode()
|
||
}
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<div class="search-registry-entry-block">
|
||
<div>44-ФЗ</div>
|
||
<a href="{detail_url}">№ 0322300001726000667</a>
|
||
<div>Объект закупки</div><div>Поставка оборудования</div>
|
||
<div>Заказчик</div><div>КГБУЗ БОЛЬНИЦА</div>
|
||
<a href="{identity_url}">КГБУЗ БОЛЬНИЦА</a>
|
||
</div>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(records[0].url, detail_url)
|
||
self.assertEqual(records[0].inn, "2725006476")
|
||
self.assertEqual(records[0].ogrn, "1022701405737")
|
||
self.assertEqual(records[0].organisation_name, "КГБУЗ БОЛЬНИЦА")
|
||
self.assertEqual(client._http_client.downloaded_urls, [identity_url])
|
||
|
||
def test_procurement_card_rejects_untrusted_detail_urls(self):
|
||
loopback_identity_url = (
|
||
"http://127.0.0.1/epz/organization/view/" "info.html?inn=2725006476"
|
||
)
|
||
foreign_detail_url = (
|
||
"https://example.test/epz/order/notice/zk20/view/"
|
||
"common-info.html?regNumber=0322300001726000667"
|
||
)
|
||
client = StructuredDataClient(source="procurements_44fz")
|
||
client._http_client = _FakeHTTPClient(
|
||
{
|
||
loopback_identity_url: b"",
|
||
foreign_detail_url: b"",
|
||
}
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<div class="search-registry-entry-block">
|
||
<div>44-ФЗ</div>
|
||
<a href="{foreign_detail_url}">№ 0322300001726000667</a>
|
||
<div>Объект закупки</div><div>Поставка оборудования</div>
|
||
<div>Заказчик</div><div>КГБУЗ БОЛЬНИЦА</div>
|
||
<a href="{loopback_identity_url}">КГБУЗ БОЛЬНИЦА</a>
|
||
</div>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertNotIn("detail_url", records[0].payload)
|
||
self.assertNotIn("identity_url", records[0].payload)
|
||
self.assertEqual(client._http_client.downloaded_urls, [])
|
||
|
||
def test_procurement_card_resolves_relative_official_urls(self):
|
||
detail_href = (
|
||
"/epz/order/notice/zk20/view/"
|
||
"common-info.html?regNumber=0322300001726000667"
|
||
)
|
||
identity_href = "/epz/organization/view/info.html?organizationCode=03223000017"
|
||
client = StructuredDataClient(
|
||
source="procurements_44fz",
|
||
enrich_eis_detail_pages=False,
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<div class="search-registry-entry-block">
|
||
<div>44-ФЗ</div>
|
||
<a href="{detail_href}">№ 0322300001726000667</a>
|
||
<div>Объект закупки</div><div>Поставка оборудования</div>
|
||
<div>Заказчик</div><div>КГБУЗ БОЛЬНИЦА</div>
|
||
<a href="{identity_href}">КГБУЗ БОЛЬНИЦА</a>
|
||
</div>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(
|
||
records[0].payload["detail_url"],
|
||
f"https://zakupki.gov.ru{detail_href}",
|
||
)
|
||
self.assertEqual(
|
||
records[0].payload["identity_url"],
|
||
f"https://zakupki.gov.ru{identity_href}",
|
||
)
|
||
|
||
|
||
class StructuredDataClientEisCardParsingTest(SimpleTestCase):
|
||
def test_contract_card_prefers_contract_details_over_order_notice(self):
|
||
contract_url = (
|
||
"https://zakupki.gov.ru/epz/contract/contractCard/"
|
||
"common-info.html?reestrNumber=3310300251025000007"
|
||
)
|
||
notice_url = (
|
||
"https://zakupki.gov.ru/epz/order/notice/view/"
|
||
"common-info.html?regNumber=0126300014725000080"
|
||
)
|
||
client = StructuredDataClient(
|
||
source="contracts",
|
||
enrich_eis_detail_pages=False,
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content=f"""
|
||
<div class="search-registry-entry-block">
|
||
<a href="{contract_url}">№ 3310300251025000007</a>
|
||
<div>Заказчик</div><div>МБДОУ СКАЗКА</div>
|
||
<div>Объекты закупки</div><div>Мясо птицы</div>
|
||
<div>Размещен контракт в реестре контрактов</div>
|
||
<div>04.12.2025</div>
|
||
<a href="{notice_url}">Сведения закупки</a>
|
||
</div>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(records[0].url, contract_url)
|
||
self.assertEqual(records[0].organisation_name, "МБДОУ СКАЗКА")
|
||
self.assertEqual(records[0].record_date, "04.12.2025")
|
||
|
||
def test_unfair_supplier_skips_empty_placed_label(self):
|
||
client = StructuredDataClient(
|
||
source="unfair_suppliers",
|
||
enrich_eis_detail_pages=False,
|
||
)
|
||
|
||
records = client.fetch_records(
|
||
content="""
|
||
<div class="search-registry-entry-block">
|
||
<div>44-ФЗ</div>
|
||
<div>№ 26007353</div>
|
||
<div>Размещено</div>
|
||
<div>Наименование (ФИО) недобросовестного поставщика</div>
|
||
<div>ООО ПРОГРЕСС</div>
|
||
<div>ИНН (аналог ИНН)</div><div>9728104322</div>
|
||
<div>Включено</div><div>14.07.2026</div>
|
||
</div>
|
||
""".encode(),
|
||
file_name="results.html",
|
||
)
|
||
|
||
self.assertEqual(records[0].title, "ООО ПРОГРЕСС")
|
||
self.assertEqual(records[0].organisation_name, "ООО ПРОГРЕСС")
|
||
self.assertEqual(records[0].record_date, "14.07.2026")
|