feat(parsers): добавлен API клиент для checko.ru

- Реализован CheckoClient с поддержкой всех 10 эндпоинтов API v2
- Frozen dataclass модели для запросов и ответов
- Справочники ОКВЭД2, ОКФС, ОКОПФ, ОКПД, статусы компаний
- Маппинг русских полей API на английские имена
- Unit тесты с моками
- E2E тесты с реальными запросами
- Настройка CHECKO_API_KEY в settings.py
This commit is contained in:
2026-02-03 17:00:19 +01:00
parent 5c88c6466d
commit c36c7b9ba9
22 changed files with 5943 additions and 4 deletions

View File

@@ -0,0 +1,631 @@
"""Tests for Checko API client."""
import json
from pathlib import Path
from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase, tag
from apps.parsers.clients.checko import (
CheckoClient,
CheckoAPIError,
CheckoNotFoundError,
CheckoRateLimitError,
CheckoValidationError,
CompanyRequest,
ContractsRequest,
EntrepreneurRequest,
FinancesRequest,
LegalCasesRequest,
PersonRequest,
SearchRequest,
SearchType,
ObjectType,
ContractLaw,
)
from apps.parsers.clients.checko.datasets import (
OKVED2,
OKFS,
OKOPF,
OKPD,
OKPD2,
AccountCodes,
CompanyStatuses,
EntrepreneurStatuses,
)
class CheckoClientInitTest(SimpleTestCase):
"""Tests for CheckoClient initialization."""
def test_client_initialization_default(self):
"""Test client initializes with defaults."""
client = CheckoClient(api_key="test_key")
self.assertEqual(client.api_key, "test_key")
self.assertEqual(client.base_url, "https://api.checko.ru/v2")
self.assertEqual(client.timeout, 30)
self.assertIsNone(client.proxies)
def test_client_initialization_custom(self):
"""Test client initializes with custom params."""
client = CheckoClient(
api_key="test_key",
base_url="https://custom.api.com",
timeout=60,
proxies=["http://proxy:8080"],
)
self.assertEqual(client.base_url, "https://custom.api.com")
self.assertEqual(client.timeout, 60)
self.assertEqual(client.proxies, ["http://proxy:8080"])
def test_context_manager(self):
"""Test client works as context manager."""
with CheckoClient(api_key="test_key") as client:
self.assertIsInstance(client, CheckoClient)
class CheckoClientValidationTest(SimpleTestCase):
"""Tests for request validation."""
def setUp(self):
self.client = CheckoClient(api_key="test_key")
def test_company_request_requires_identifier(self):
"""Test CompanyRequest requires at least one identifier."""
with self.assertRaises(CheckoValidationError) as context:
self.client.get_company(CompanyRequest())
self.assertIn("ogrn", str(context.exception).lower())
def test_entrepreneur_request_requires_identifier(self):
"""Test EntrepreneurRequest requires at least one identifier."""
with self.assertRaises(CheckoValidationError) as context:
self.client.get_entrepreneur(EntrepreneurRequest())
self.assertIn("ogrn", str(context.exception).lower())
def test_search_request_min_query_length(self):
"""Test SearchRequest validates query length."""
with self.assertRaises(CheckoValidationError) as context:
self.client.search(
SearchRequest(
by=SearchType.NAME,
obj=ObjectType.ORGANIZATION,
query="abc", # Too short
)
)
self.assertIn("4", str(context.exception))
def test_finances_request_requires_identifier(self):
"""Test FinancesRequest requires at least one identifier."""
with self.assertRaises(CheckoValidationError) as context:
self.client.get_finances(FinancesRequest())
self.assertIn("ogrn", str(context.exception).lower())
class CheckoClientApiTest(SimpleTestCase):
"""Tests for API requests with mocked responses."""
def setUp(self):
self.client = CheckoClient(api_key="test_key")
@patch.object(CheckoClient, "_request")
def test_get_company_success(self, mock_request):
"""Test successful company retrieval."""
mock_request.return_value = {
"data": {
"ogrn": "1027700132195",
"inn": "7707083893",
"kpp": "773601001",
"full_name": "ПУБЛИЧНОЕ АКЦИОНЕРНОЕ ОБЩЕСТВО \"СБЕРБАНК РОССИИ\"",
"short_name": "ПАО Сбербанк",
"reg_date": "1991-06-20",
"status": {
"restricted_access": False,
"code": "100",
"name": "Действующее",
},
"legal_address": {
"restricted_access": False,
"full_address": "г Москва, ул Вавилова, д 19",
},
},
"meta": {
"status": "ok",
"today_request_count": 1,
"balance": 99.90,
},
}
response = self.client.get_company(CompanyRequest(inn="7707083893"))
self.assertEqual(response.meta.status, "ok")
self.assertEqual(response.data.inn, "7707083893")
self.assertEqual(response.data.short_name, "ПАО Сбербанк")
self.assertEqual(response.data.status.code, "100")
@patch.object(CheckoClient, "_request")
def test_get_company_not_found(self, mock_request):
"""Test company not found error."""
mock_request.side_effect = CheckoNotFoundError(
message="Организация не найдена",
balance=99.0,
)
with self.assertRaises(CheckoNotFoundError):
self.client.get_company(CompanyRequest(inn="0000000000"))
@patch.object(CheckoClient, "_request")
def test_get_entrepreneur_success(self, mock_request):
"""Test successful entrepreneur retrieval."""
mock_request.return_value = {
"data": {
"ogrnip": "304770000000001",
"inn": "770100000001",
"full_name": "Иванов Иван Иванович",
"reg_date": "2010-01-15",
"status": {
"code": "100",
"name": "Действующий",
},
},
"meta": {
"status": "ok",
"today_request_count": 2,
"balance": 99.80,
},
}
response = self.client.get_entrepreneur(
EntrepreneurRequest(inn="770100000001")
)
self.assertEqual(response.data.ogrnip, "304770000000001")
self.assertEqual(response.data.full_name, "Иванов Иван Иванович")
@patch.object(CheckoClient, "_request")
def test_search_organizations(self, mock_request):
"""Test organization search."""
mock_request.return_value = {
"data": {
"organizations": [
{
"ogrn": "1027700132195",
"inn": "7707083893",
"short_name": "ПАО Сбербанк",
"status": "Действующее",
},
{
"ogrn": "1027700000000",
"inn": "7700000000",
"short_name": "Сбербанк Капитал",
"status": "Действующее",
},
],
"pagination": {
"total_records": 2,
"total_pages": 1,
"current_page": 1,
},
},
"meta": {
"status": "ok",
"today_request_count": 3,
"balance": 99.70,
},
}
response = self.client.search(
SearchRequest(
by=SearchType.NAME,
obj=ObjectType.ORGANIZATION,
query="Сбербанк",
)
)
self.assertEqual(len(response.data.organizations), 2)
self.assertEqual(response.data.organizations[0].inn, "7707083893")
self.assertEqual(response.data.pagination.total_records, 2)
@patch.object(CheckoClient, "_request")
def test_get_finances(self, mock_request):
"""Test financial data retrieval."""
mock_request.return_value = {
"data": {
"ogrn": "1027700132195",
"inn": "7707083893",
"reports": [
{
"year": 2023,
"balance": [
{"code": "1100", "current": 1000000, "previous": 900000},
{"code": "1200", "current": 500000, "previous": 450000},
],
"profit_loss": [
{"code": "2110", "current": 2000000,
"previous": 1800000},
],
}
],
"summary": {
"revenue": 2000000,
"profit": 500000,
"assets": 1500000,
},
},
"meta": {
"status": "ok",
"today_request_count": 4,
"balance": 99.60,
},
}
response = self.client.get_finances(FinancesRequest(inn="7707083893"))
self.assertEqual(len(response.data.reports), 1)
self.assertEqual(response.data.reports[0].year, 2023)
self.assertEqual(response.data.summary.revenue, 2000000)
@patch.object(CheckoClient, "_request")
def test_get_contracts(self, mock_request):
"""Test contracts retrieval."""
mock_request.return_value = {
"data": {
"contracts": [
{
"registry_number": "0123456789012345",
"publish_date": "2024-01-15",
"price": 1000000,
"status": "Исполнение",
"subject": "Поставка оборудования",
"law": "44",
}
],
"pagination": {
"total_records": 1,
"total_pages": 1,
"current_page": 1,
},
"total_sum": 1000000,
},
"meta": {
"status": "ok",
"today_request_count": 5,
"balance": 99.50,
},
}
response = self.client.get_contracts(
ContractsRequest(inn="7707083893", law=ContractLaw.FZ44)
)
self.assertEqual(len(response.data.contracts), 1)
self.assertEqual(response.data.contracts[0].price, 1000000)
self.assertEqual(response.data.total_sum, 1000000)
@patch.object(CheckoClient, "_request")
def test_get_legal_cases(self, mock_request):
"""Test legal cases retrieval."""
mock_request.return_value = {
"data": {
"cases": [
{
"case_number": "А40-12345/2024",
"court_name": "Арбитражный суд г. Москвы",
"claim_amount": 5000000,
"status": "Рассмотрение дела",
"plaintiffs": [{"name": "ООО Истец", "inn": "1234567890"}],
"defendants": [{"name": "ООО Ответчик", "inn": "0987654321"}],
}
],
"pagination": {
"total_records": 1,
"total_pages": 1,
"current_page": 1,
},
"total_claim_amount": 5000000,
},
"meta": {
"status": "ok",
"today_request_count": 6,
"balance": 99.40,
},
}
response = self.client.get_legal_cases(
LegalCasesRequest(inn="7707083893"))
self.assertEqual(len(response.data.cases), 1)
self.assertEqual(response.data.cases[0].case_number, "А40-12345/2024")
self.assertEqual(len(response.data.cases[0].plaintiffs), 1)
class CheckoClientErrorHandlingTest(SimpleTestCase):
"""Tests for error handling."""
def setUp(self):
self.client = CheckoClient(api_key="test_key")
@patch("apps.parsers.clients.checko.client.BaseHTTPClient.get_json")
def test_api_error_handling(self, mock_get_json):
"""Test API error response handling."""
mock_get_json.return_value = {
"meta": {
"status": "error",
"message": "Invalid API key",
"balance": 0,
"today_request_count": 0,
}
}
with self.assertRaises(CheckoAPIError) as context:
self.client.get_company(CompanyRequest(inn="7707083893"))
self.assertIn("Invalid API key", str(context.exception))
@patch("apps.parsers.clients.checko.client.BaseHTTPClient.get_json")
def test_rate_limit_error_handling(self, mock_get_json):
"""Test rate limit error detection."""
mock_get_json.return_value = {
"meta": {
"status": "error",
"message": "Превышен лимит запросов",
"balance": 0,
"today_request_count": 100,
}
}
with self.assertRaises(CheckoRateLimitError):
self.client.get_company(CompanyRequest(inn="7707083893"))
@patch("apps.parsers.clients.checko.client.BaseHTTPClient.get_json")
def test_not_found_error_handling(self, mock_get_json):
"""Test not found error detection."""
mock_get_json.return_value = {
"meta": {
"status": "error",
"message": "Организация не найдена",
"balance": 99.0,
"today_request_count": 1,
}
}
with self.assertRaises(CheckoNotFoundError):
self.client.get_company(CompanyRequest(inn="0000000000"))
class CheckoRequestModelsTest(SimpleTestCase):
"""Tests for request dataclass models."""
def test_company_request_to_params(self):
"""Test CompanyRequest.to_params()."""
request = CompanyRequest(inn="7707083893", source=True)
params = request.to_params()
self.assertEqual(params["inn"], "7707083893")
self.assertEqual(params["source"], "true")
self.assertNotIn("ogrn", params)
def test_search_request_to_params(self):
"""Test SearchRequest.to_params()."""
request = SearchRequest(
by=SearchType.NAME,
obj=ObjectType.ORGANIZATION,
query="Сбербанк",
region="77",
active=True,
limit=50,
page=2,
)
params = request.to_params()
self.assertEqual(params["by"], "name")
self.assertEqual(params["obj"], "org")
self.assertEqual(params["query"], "Сбербанк")
self.assertEqual(params["region"], "77")
self.assertEqual(params["active"], "true")
self.assertEqual(params["limit"], "50")
self.assertEqual(params["page"], "2")
def test_contracts_request_to_params(self):
"""Test ContractsRequest.to_params()."""
request = ContractsRequest(
inn="7707083893",
law=ContractLaw.FZ44,
)
params = request.to_params()
self.assertEqual(params["inn"], "7707083893")
self.assertEqual(params["law"], "44")
def test_legal_cases_request_to_params(self):
"""Test LegalCasesRequest.to_params()."""
request = LegalCasesRequest(
inn="7707083893",
actual=True,
active=True,
date_from="2024-01-01",
claim_amount_from=1000000,
)
params = request.to_params()
self.assertEqual(params["inn"], "7707083893")
self.assertEqual(params["actual"], "true")
self.assertEqual(params["active"], "true")
self.assertEqual(params["date_from"], "2024-01-01")
self.assertEqual(params["claim_amount_from"], "1000000")
@tag("datasets")
class CheckoDatasetsTest(SimpleTestCase):
"""Tests for reference datasets."""
def test_okved2_get(self):
"""Test OKVED2 dataset get by code."""
item = OKVED2.get("62.01")
self.assertIsNotNone(item)
self.assertEqual(item.code, "62.01")
self.assertIn("программ", item.name.lower())
def test_okved2_get_name(self):
"""Test OKVED2 dataset get_name."""
name = OKVED2.get_name("62.01")
self.assertIsNotNone(name)
self.assertIn("программ", name.lower())
def test_okved2_search(self):
"""Test OKVED2 search functionality."""
results = OKVED2.search("программ")
self.assertGreater(len(results), 0)
for item in results:
self.assertIn("программ", item.name.lower())
def test_okved2_exists(self):
"""Test OKVED2 exists check."""
self.assertTrue(OKVED2.exists("62.01"))
self.assertFalse(OKVED2.exists("99.99.99"))
def test_okved2_get_children(self):
"""Test OKVED2 hierarchy - get children."""
children = OKVED2.get_children("62")
self.assertGreater(len(children), 0)
for child in children:
self.assertTrue(child.code.startswith("62."))
def test_okfs_get(self):
"""Test OKFS dataset."""
item = OKFS.get("12")
self.assertIsNotNone(item)
self.assertEqual(item.code, "12")
def test_okfs_get_name(self):
"""Test OKFS get_name."""
name = OKFS.get_name("12")
self.assertIsNotNone(name)
def test_okopf_get(self):
"""Test OKOPF dataset."""
# Check for common OPF code
item = OKOPF.get("12300") # ООО
if item:
self.assertEqual(item.code, "12300")
def test_account_codes_get(self):
"""Test AccountCodes dataset."""
item = AccountCodes.get("1100")
self.assertIsNotNone(item)
self.assertEqual(item.code, "1100")
def test_company_statuses_get(self):
"""Test CompanyStatuses dataset."""
# Should return builtin value if no JSON
name = CompanyStatuses.get_name("100")
# May be None if no data, but shouldn't raise
self.assertTrue(name is None or isinstance(name, str))
def test_entrepreneur_statuses_get(self):
"""Test EntrepreneurStatuses dataset."""
name = EntrepreneurStatuses.get_name("100")
# May be None if no data, but shouldn't raise
self.assertTrue(name is None or isinstance(name, str))
def test_okpd_get(self):
"""Test OKPD dataset."""
# Check all() works
items = OKPD.all()
self.assertIsInstance(items, list)
def test_okpd2_get(self):
"""Test OKPD2 dataset."""
items = OKPD2.all()
self.assertIsInstance(items, list)
class CheckoClientIteratorsTest(SimpleTestCase):
"""Tests for paginated iterators."""
def setUp(self):
self.client = CheckoClient(api_key="test_key")
@patch.object(CheckoClient, "_request")
def test_iter_contracts_pagination(self, mock_request):
"""Test contracts iterator handles pagination."""
# First page
mock_request.side_effect = [
{
"data": {
"contracts": [
{"registry_number": "0001", "price": 100},
{"registry_number": "0002", "price": 200},
],
"pagination": {
"total_records": 4,
"total_pages": 2,
"current_page": 1,
},
},
"meta": {"status": "ok", "today_request_count": 1, "balance": 99},
},
# Second page
{
"data": {
"contracts": [
{"registry_number": "0003", "price": 300},
{"registry_number": "0004", "price": 400},
],
"pagination": {
"total_records": 4,
"total_pages": 2,
"current_page": 2,
},
},
"meta": {"status": "ok", "today_request_count": 2, "balance": 98},
},
]
contracts = list(
self.client.iter_contracts(
ContractsRequest(inn="7707083893", law=ContractLaw.FZ44)
)
)
self.assertEqual(len(contracts), 4)
self.assertEqual(contracts[0].registry_number, "0001")
self.assertEqual(contracts[3].registry_number, "0004")
@patch.object(CheckoClient, "_request")
def test_iter_legal_cases_empty(self, mock_request):
"""Test legal cases iterator handles empty results."""
mock_request.return_value = {
"data": {
"cases": [],
"pagination": {
"total_records": 0,
"total_pages": 0,
"current_page": 1,
},
},
"meta": {"status": "ok", "today_request_count": 1, "balance": 99},
}
cases = list(
self.client.iter_legal_cases(LegalCasesRequest(inn="0000000000"))
)
self.assertEqual(len(cases), 0)