Files
mostovik-backend/tests/apps/parsers/test_views.py
Aleksandr Meshchriakov 7879d54958
Some checks failed
CI/CD Pipeline / Code Quality Checks (pull_request) Failing after 10m20s
CI/CD Pipeline / Run Tests (pull_request) Failing after 11m5s
CI/CD Pipeline / Build Docker Images (pull_request) Has been skipped
CI/CD Pipeline / Push to Gitea Registry (pull_request) Has been skipped
feat: add parser source dashboard and scheduling
2026-04-27 23:56:16 +02:00

754 lines
31 KiB
Python

"""Tests for parsers API views."""
import json
from unittest.mock import Mock, patch
from apps.parsers.models import ParserLoadLog
from apps.user.services import UserService
from django.core.files.uploadedfile import SimpleUploadedFile
from django.urls import reverse
from django_celery_beat.models import PeriodicTask
from rest_framework import status
from rest_framework.test import APITestCase
from tests.apps.parsers.factories import (
GenericParserRecordFactory,
IndustrialCertificateRecordFactory,
InspectionRecordFactory,
ManufacturerRecordFactory,
ParserLoadLogFactory,
)
from tests.apps.user.factories import UserFactory
class ParserApiTest(APITestCase):
"""Tests for parsers API."""
def setUp(self):
self.user = UserFactory.create_user()
self.tokens = UserService.get_tokens_for_user(self.user)
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.tokens['access']}")
def test_source_list_contains_existing_and_new_parsers(self):
"""Test source catalog exposes existing and new parser slices."""
response = self.client.get(reverse("api_v1:parsers:source-list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
keys = {item["key"] for item in response.data["data"]}
self.assertIn("industrial", keys)
self.assertIn("trudvsem", keys)
self.assertIn("fedresurs_bankruptcy", keys)
sources = {item["key"]: item for item in response.data["data"]}
self.assertEqual(sources["industrial"]["mode"], "native_api")
self.assertEqual(sources["fedresurs_bankruptcy"]["mode"], "official_api")
self.assertEqual(
sources["fedresurs_bankruptcy"]["status"],
"implemented",
)
self.assertEqual(
sources["fedresurs_bankruptcy"]["upstream_url"],
"https://bankrot.fedresurs.ru/",
)
self.assertFalse(
any(item["mode"] == "file_import" for item in sources.values())
)
self.assertEqual(sources["fns_financial"]["owner"], "Сергей")
self.assertTrue(sources["fns_financial"]["supports_file_upload"])
self.assertTrue(sources["fedresurs_bankruptcy"]["supports_file_upload"])
upload_sources = {
key for key, source in sources.items() if source["supports_file_upload"]
}
self.assertEqual(upload_sources, {"fns_financial", "fedresurs_bankruptcy"})
self.assertFalse(sources["trudvsem"]["supports_file_upload"])
self.assertFalse(sources["mpt_products"]["supports_file_upload"])
self.assertEqual(sources["mpt_products"]["upload_url"], "")
self.assertEqual(
sources["fns_financial"]["result_list_url"],
"/api/v1/fns/reports/",
)
self.assertEqual(
sources["fns_financial"]["result_detail_url"],
"/api/v1/fns/reports/{id}/",
)
self.assertEqual(
sources["fns_financial"]["upload_url"],
"/api/v1/fns/upload/",
)
self.assertEqual(
sources["procurements_44fz"]["result_list_url"],
"/api/v1/zakupki/",
)
self.assertEqual(
sources["procurements_223fz"]["result_list_url"],
"/api/v1/zakupki/",
)
self.assertEqual(sources["contracts"]["result_list_url"], "/api/v1/zakupki/")
def test_dashboard_page_is_available_outside_admin(self):
"""Test dashboard HTML shell is served outside Django admin."""
response = self.client.get(reverse("dashboard"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertContains(response, "Parser Dashboard")
self.assertContains(response, 'name="username"')
self.assertContains(response, "color-scheme: dark")
self.assertContains(response, "refreshTokenButton")
self.assertContains(response, "sourceRoutePanel")
self.assertContains(response, "uploadPanel")
self.assertContains(response, "uploadSources")
self.assertContains(response, "registryUploadPanel")
self.assertContains(response, "Загрузка организаций ОПК")
self.assertContains(response, "/api/v1/registers/upload/")
self.assertContains(response, "/api/v1/registers/registries/")
self.assertContains(response, "Внешняя БД и выгрузка данных")
self.assertContains(response, "/api/v1/exchange/copy/")
self.assertContains(response, "Выгрузка организаций реестра в .bin")
self.assertContains(response, "/api/v1/backups/export/")
self.assertContains(response, "Источники API")
self.assertContains(response, "Загрузить реестр")
self.assertContains(response, "/api/v1/users/token/refresh/")
def test_dashboard_nested_routes_render_same_html_shell(self):
"""Test direct source and item dashboard URLs render the HTML app."""
source_response = self.client.get("/dashboard/trudvsem")
item_response = self.client.get("/dashboard/trudvsem/1")
self.assertEqual(source_response.status_code, status.HTTP_200_OK)
self.assertEqual(item_response.status_code, status.HTTP_200_OK)
self.assertContains(source_response, "Parser Dashboard")
self.assertContains(item_response, "Parser Dashboard")
@patch("apps.parsers.views.tasks.parse_trudvsem_vacancies.delay")
def test_run_parser_starts_celery_task(self, mock_delay):
"""Test parser run endpoint starts task through Celery."""
mock_delay.return_value = Mock(id="task-123")
response = self.client.post(
reverse("api_v1:parsers:run-parser", kwargs={"source_key": "trudvsem"}),
{"limit": 10, "text": "инженер"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "task-123")
mock_delay.assert_called_once()
self.assertEqual(mock_delay.call_args.kwargs["limit"], 10)
self.assertEqual(mock_delay.call_args.kwargs["user_id"], self.user.id)
@patch("apps.parsers.views.tasks.parse_industrial_production.delay")
def test_run_existing_parser_passes_user_id(self, mock_delay):
"""Test existing parser tasks keep API job ownership."""
mock_delay.return_value = Mock(id="task-existing")
response = self.client.post(
reverse("api_v1:parsers:run-parser", kwargs={"source_key": "industrial"}),
{},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "task-existing")
self.assertEqual(mock_delay.call_args.kwargs["user_id"], self.user.id)
@patch("apps.parsers.views.tasks.sync_inspections.delay")
def test_run_sync_inspections_passes_control_params(self, mock_delay):
"""Test sync parser accepts bounded run controls from API."""
mock_delay.return_value = Mock(id="task-sync")
response = self.client.post(
reverse(
"api_v1:parsers:run-parser",
kwargs={"source_key": "sync_inspections"},
),
{
"max_months_per_law": 1,
"start_year": 2026,
"start_month": 1,
"include_fz294": True,
"include_fz248": False,
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "task-sync")
self.assertEqual(mock_delay.call_args.kwargs["max_months_per_law"], 1)
self.assertEqual(mock_delay.call_args.kwargs["start_year"], 2026)
self.assertEqual(mock_delay.call_args.kwargs["start_month"], 1)
self.assertTrue(mock_delay.call_args.kwargs["include_fz294"])
self.assertFalse(mock_delay.call_args.kwargs["include_fz248"])
@patch("apps.parsers.views.tasks.parse_fedresurs_bankruptcy.delay")
def test_run_upstream_parser_uses_catalog_source_without_file_url(self, mock_delay):
"""Test upstream parsers do not require manual file URLs."""
mock_delay.return_value = Mock(id="fedresurs-task")
response = self.client.post(
reverse(
"api_v1:parsers:run-parser",
kwargs={"source_key": "fedresurs_bankruptcy"},
),
{},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "fedresurs-task")
self.assertNotIn("file_url", mock_delay.call_args.kwargs)
def test_run_file_parser_rejects_private_file_url(self):
"""Test parser run endpoint blocks private worker-side URLs."""
response = self.client.post(
reverse(
"api_v1:parsers:run-parser",
kwargs={"source_key": "fedresurs_bankruptcy"},
),
{"file_url": "https://127.0.0.1/private.xml"},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_run_parser_rejects_proxy_override_for_regular_user(self):
"""Test non-staff users cannot route parser traffic through own proxy."""
response = self.client.post(
reverse("api_v1:parsers:run-parser", kwargs={"source_key": "industrial"}),
{"proxies": ["http://proxy.example:8080"]},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(
response.data["errors"][0]["code"],
"proxy_override_forbidden",
)
@patch("apps.parsers.views._save_uploaded_parser_file")
@patch("apps.parsers.views.tasks.import_parser_upload.delay")
def test_upload_registry_file_starts_celery_task(
self,
mock_delay,
mock_save_upload,
):
"""Test manual registry upload starts Celery import task."""
mock_delay.return_value = Mock(id="upload-task")
mock_save_upload.return_value = "parser_uploads/source.csv"
uploaded_file = SimpleUploadedFile(
"source.csv",
b"inn,ogrn,name\n1234567890,1234567890123,Test Org\n",
content_type="text/csv",
)
response = self.client.post(
reverse(
"api_v1:parsers:upload-parser-data",
kwargs={"source_key": "fns_financial"},
),
{"file": uploaded_file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "upload-task")
mock_delay.assert_called_once_with(
source_key="fns_financial",
storage_path="parser_uploads/source.csv",
file_name="source.csv",
user_id=self.user.id,
)
@patch("apps.parsers.views._save_uploaded_parser_file")
@patch("apps.parsers.views.tasks.import_parser_upload.delay")
def test_source_upload_registry_file_starts_celery_task(
self,
mock_delay,
mock_save_upload,
):
"""Test source upload endpoint follows dev-compatible FNS route."""
mock_delay.return_value = Mock(id="upload-task")
mock_save_upload.return_value = "parser_uploads/source.csv"
uploaded_file = SimpleUploadedFile(
"source.csv",
b"inn,ogrn,name\n1234567890,1234567890123,Test Org\n",
content_type="text/csv",
)
response = self.client.post(
"/api/v1/fns/upload/",
{"file": uploaded_file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["data"]["task_id"], "upload-task")
mock_delay.assert_called_once_with(
source_key="fns_financial",
storage_path="parser_uploads/source.csv",
file_name="source.csv",
user_id=self.user.id,
)
def test_source_upload_absent_for_non_upload_source(self):
"""Test source API does not expose upload where source has no manual import."""
response = self.client.post("/api/v1/trudvsem/vacancies/upload/", {})
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_upload_registry_file_rejects_non_upload_source(self):
"""Test manual upload is limited to sources that support file import."""
uploaded_file = SimpleUploadedFile(
"source.csv",
b"inn,name\n1234567890,Test Org\n",
content_type="text/csv",
)
response = self.client.post(
reverse(
"api_v1:parsers:upload-parser-data",
kwargs={"source_key": "trudvsem"},
),
{"file": uploaded_file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["errors"][0]["code"], "upload_not_supported")
def test_upload_registry_file_rejects_api_only_registry_source(self):
"""Test API/upstream sources do not expose manual file imports."""
uploaded_file = SimpleUploadedFile(
"source.csv",
b"inn,name\n1234567890,Test Org\n",
content_type="text/csv",
)
response = self.client.post(
reverse(
"api_v1:parsers:upload-parser-data",
kwargs={"source_key": "mpt_products"},
),
{"file": uploaded_file},
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["errors"][0]["code"], "upload_not_supported")
def test_load_logs_list(self):
"""Test load logs endpoint."""
ParserLoadLogFactory(source=ParserLoadLog.Source.FNS_FINANCIAL, batch_id=1)
response = self.client.get(reverse("api_v1:parsers:load-log-list"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
def test_load_logs_list_rejects_invalid_limit(self):
"""Test load logs endpoint validates limit query parameter."""
response = self.client.get(
reverse("api_v1:parsers:load-log-list"),
{"limit": "bad"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_frontend_source_card_aliases(self):
"""Test dev frontend source-card API aliases are present."""
GenericParserRecordFactory(source=ParserLoadLog.Source.FNS_FINANCIAL)
ParserLoadLogFactory(source=ParserLoadLog.Source.FNS_FINANCIAL, batch_id=1)
list_response = self.client.get(reverse("api_v1:sources:source-cards-list"))
statuses_response = self.client.get(reverse("api_v1:sources:source-statuses"))
detail_response = self.client.get(
reverse(
"api_v1:sources:source-card-detail",
kwargs={"slug": "financial-indicators"},
)
)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(statuses_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
slugs = {item["slug"] for item in list_response.data["data"]}
self.assertIn("financial-indicators", slugs)
self.assertIn("manufacturers-and-products", slugs)
self.assertIn("public-procurements", slugs)
self.assertEqual(detail_response.data["data"]["slug"], "financial-indicators")
self.assertIn("source_items", detail_response.data["data"])
def test_frontend_parsing_settings_alias_get_and_patch(self):
"""Test dev-compatible parsing settings endpoint."""
url = reverse("api_v1:parsing:parsing-settings")
get_response = self.client.get(url)
patch_response = self.client.patch(
url,
{"planned_inspections": "weekly"},
format="json",
)
self.assertEqual(get_response.status_code, status.HTTP_200_OK)
self.assertEqual(patch_response.status_code, status.HTTP_200_OK)
self.assertEqual(patch_response.data["planned_inspections"], "weekly")
def test_frontend_source_refresh_alias_starts_task(self):
"""Test dev-compatible source refresh endpoint starts mapped Celery task."""
task = Mock()
task.delay.return_value = Mock(id="compat-refresh-task")
with patch.dict(
"apps.parsers.frontend_compat.TASKS_BY_NAME",
{"apps.parsers.tasks.parse_trudvsem_vacancies": task},
):
response = self.client.post(
reverse(
"api_v1:sources:source-card-refresh",
kwargs={"slug": "labor-vacancies"},
),
{"params": {"limit": 5, "text": "engineer"}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED)
self.assertEqual(response.data["task_id"], "compat-refresh-task")
task.delay.assert_called_once_with(
limit=5,
text="engineer",
user_id=self.user.id,
)
def test_frontend_source_refresh_rejects_nested_proxy_override(self):
"""Test source refresh alias keeps proxy override restricted."""
response = self.client.post(
reverse(
"api_v1:sources:source-card-refresh",
kwargs={"slug": "labor-vacancies"},
),
{"params": {"proxies": ["http://proxy.example"]}},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.data["errors"][0]["code"], "proxy_override_forbidden")
def test_frontend_system_logs_aliases(self):
"""Test dev-compatible system logs list/detail/export endpoints."""
log = ParserLoadLogFactory(
source=ParserLoadLog.Source.FNS_FINANCIAL,
batch_id=777,
records_count=3,
)
list_response = self.client.get(
reverse("api_v1:system:parser-logs-list"),
{"search": "777"},
)
detail_response = self.client.get(
reverse("api_v1:system:parser-logs-detail", kwargs={"pk": log.id})
)
export_response = self.client.get(
reverse("api_v1:system:parser-logs-export"),
{"search": "777"},
)
self.assertEqual(list_response.status_code, status.HTTP_200_OK)
self.assertEqual(list_response.data["count"], 1)
self.assertEqual(
list_response.data["results"][0]["source"], "financial-indicators"
)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["id"], log.id)
self.assertEqual(export_response.status_code, status.HTTP_200_OK)
self.assertIn("text/csv", export_response["Content-Type"])
def test_generic_records_list_filters_by_source(self):
"""Test generic records endpoint filters by source."""
GenericParserRecordFactory(source=ParserLoadLog.Source.TRUDVSEM)
GenericParserRecordFactory(source=ParserLoadLog.Source.FNS_FINANCIAL)
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"source": ParserLoadLog.Source.TRUDVSEM},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(
response.data["data"][0]["source"], ParserLoadLog.Source.TRUDVSEM
)
def test_generic_records_list_filters_by_id(self):
"""Test generic records endpoint can support dashboard detail view."""
target = GenericParserRecordFactory(source=ParserLoadLog.Source.TRUDVSEM)
GenericParserRecordFactory(source=ParserLoadLog.Source.TRUDVSEM)
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"source": ParserLoadLog.Source.TRUDVSEM, "id": target.id},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["id"], target.id)
def test_records_list_returns_old_industrial_records(self):
"""Test records endpoint still exposes old industrial parser data."""
target = IndustrialCertificateRecordFactory()
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"source": ParserLoadLog.Source.INDUSTRIAL, "id": target.id},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
record = response.data["data"][0]
self.assertEqual(record["source"], ParserLoadLog.Source.INDUSTRIAL)
self.assertEqual(record["external_id"], target.certificate_number)
self.assertEqual(record["organisation_name"], target.organisation_name)
self.assertEqual(
record["payload"]["certificate_number"], target.certificate_number
)
def test_records_list_returns_old_manufacturer_records(self):
"""Test records endpoint still exposes old manufacturer parser data."""
target = ManufacturerRecordFactory()
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"source": ParserLoadLog.Source.MANUFACTURES, "id": target.id},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
record = response.data["data"][0]
self.assertEqual(record["source"], ParserLoadLog.Source.MANUFACTURES)
self.assertEqual(record["external_id"], target.inn)
self.assertEqual(record["organisation_name"], target.full_legal_name)
def test_records_list_returns_old_inspection_records(self):
"""Test records endpoint still exposes old inspections parser data."""
target = InspectionRecordFactory()
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"source": ParserLoadLog.Source.INSPECTIONS, "id": target.id},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
record = response.data["data"][0]
self.assertEqual(record["source"], ParserLoadLog.Source.INSPECTIONS)
self.assertEqual(record["external_id"], target.registration_number)
self.assertEqual(record["organisation_name"], target.organisation_name)
self.assertEqual(record["status"], target.status)
def test_generic_records_list_rejects_invalid_id(self):
"""Test generic records endpoint validates id filter."""
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"id": "bad"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(response.data["errors"][0]["code"], "invalid_record_id")
def test_generic_records_list_rejects_invalid_limit(self):
"""Test generic records endpoint validates limit query parameter."""
response = self.client.get(
reverse("api_v1:parsers:generic-record-list"),
{"limit": "bad"},
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_source_result_list_filters_generic_records(self):
"""Test source result API exposes get_list with query params."""
target = GenericParserRecordFactory(
source=ParserLoadLog.Source.FNS_FINANCIAL,
inn="7701000001",
organisation_name="Target Org",
)
GenericParserRecordFactory(
source=ParserLoadLog.Source.FNS_FINANCIAL,
inn="7701000002",
)
GenericParserRecordFactory(source=ParserLoadLog.Source.TRUDVSEM)
response = self.client.get(
"/api/v1/fns/reports/",
{"inn": "7701000001", "page_size": 10, "include_payload": "false"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["id"], target.id)
self.assertEqual(response.data["data"][0]["payload"], {})
self.assertEqual(response.data["meta"]["pagination"]["total_count"], 1)
def test_source_result_get_filters_generic_record(self):
"""Test source result API exposes get with query params."""
target = GenericParserRecordFactory(
source=ParserLoadLog.Source.FNS_FINANCIAL,
inn="7701000001",
)
response = self.client.get(
f"/api/v1/fns/reports/{target.id}/",
{"inn": "7701000001"},
)
missing_response = self.client.get(
f"/api/v1/fns/reports/{target.id}/",
{"inn": "7701000002"},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["data"]["id"], target.id)
self.assertEqual(missing_response.status_code, status.HTTP_404_NOT_FOUND)
def test_source_result_list_filters_native_records(self):
"""Test source result API exposes old native records with the same result DTO."""
target = ManufacturerRecordFactory(inn="7701000001")
ManufacturerRecordFactory(inn="7701000002")
response = self.client.get(
"/api/v1/minpromtorg/manufacturers/",
{"inn": "7701000001", "page_size": 10},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["data"]), 1)
self.assertEqual(response.data["data"][0]["id"], target.id)
self.assertEqual(
response.data["data"][0]["organisation_name"],
target.full_legal_name,
)
def test_zakupki_result_route_groups_eis_sources(self):
"""Test EIS data is exposed through one dev-compatible zakupki route."""
target = GenericParserRecordFactory(
source=ParserLoadLog.Source.PROCUREMENTS_44FZ,
inn="7701000001",
organisation_name="44FZ Org",
)
GenericParserRecordFactory(
source=ParserLoadLog.Source.PROCUREMENTS_223FZ,
inn="7701000002",
organisation_name="223FZ Org",
)
GenericParserRecordFactory(
source=ParserLoadLog.Source.CONTRACTS,
inn="7701000003",
organisation_name="Contract Org",
)
GenericParserRecordFactory(source=ParserLoadLog.Source.FNS_FINANCIAL)
grouped_response = self.client.get("/api/v1/zakupki/", {"page_size": 10})
filtered_response = self.client.get(
"/api/v1/zakupki/",
{"source": ParserLoadLog.Source.PROCUREMENTS_44FZ, "page_size": 10},
)
detail_response = self.client.get(f"/api/v1/zakupki/{target.id}/")
self.assertEqual(grouped_response.status_code, status.HTTP_200_OK)
self.assertEqual(
{record["source"] for record in grouped_response.data["data"]},
{
ParserLoadLog.Source.PROCUREMENTS_44FZ,
ParserLoadLog.Source.PROCUREMENTS_223FZ,
ParserLoadLog.Source.CONTRACTS,
},
)
self.assertEqual(filtered_response.status_code, status.HTTP_200_OK)
self.assertEqual(len(filtered_response.data["data"]), 1)
self.assertEqual(filtered_response.data["data"][0]["id"], target.id)
self.assertEqual(detail_response.status_code, status.HTTP_200_OK)
self.assertEqual(detail_response.data["data"]["id"], target.id)
def test_invented_eis_subroutes_are_not_exposed(self):
"""Test public API does not expose invented EIS subresource routes."""
self.assertEqual(
self.client.get("/api/v1/zakupki/procurements-44fz/").status_code,
status.HTTP_404_NOT_FOUND,
)
self.assertEqual(
self.client.get("/api/v1/zakupki/procurements-223fz/").status_code,
status.HTTP_404_NOT_FOUND,
)
self.assertEqual(
self.client.get("/api/v1/zakupki/contracts/").status_code,
status.HTTP_404_NOT_FOUND,
)
self.assertEqual(
self.client.post("/api/v1/zakupki/upload/", {}).status_code,
status.HTTP_404_NOT_FOUND,
)
def test_dashboard_data_contains_sources_jobs_and_groups(self):
"""Test dashboard data endpoint returns management payload."""
ParserLoadLogFactory(source=ParserLoadLog.Source.FNS_FINANCIAL, batch_id=1)
response = self.client.get(reverse("api_v1:parsers:dashboard-data"))
self.assertEqual(response.status_code, status.HTTP_200_OK)
payload = response.data["data"]
self.assertIn("sources", payload)
self.assertIn("jobs", payload)
self.assertIn("schedules", payload)
self.assertEqual(payload["api"]["frontend_sources"], "/api/v1/sources/")
self.assertEqual(payload["api"]["system_logs"], "/api/v1/system/logs/")
financial_keys = {
item["key"] for item in payload["groups"]["financial_reports"]
}
self.assertIn("fns_financial", financial_keys)
upload_keys = {item["key"] for item in payload["groups"]["uploads"]}
self.assertEqual(upload_keys, {"fns_financial", "fedresurs_bankruptcy"})
def test_schedule_create_configures_celery_beat_task(self):
"""Test parser schedule endpoint creates django-celery-beat task."""
response = self.client.post(
reverse("api_v1:parsers:schedule-list"),
{
"source_key": "trudvsem",
"every": 6,
"period": "hours",
"limit": 25,
"text": "инженер",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["data"]["source_key"], "trudvsem")
periodic_task = PeriodicTask.objects.get(
name=f"parser:trudvsem:user:{self.user.id}"
)
kwargs = json.loads(periodic_task.kwargs)
self.assertEqual(kwargs["user_id"], self.user.id)
self.assertEqual(kwargs["limit"], 25)
self.assertEqual(periodic_task.interval.every, 6)
def test_schedule_update_and_delete(self):
"""Test parser schedule can be paused and removed."""
create_response = self.client.post(
reverse("api_v1:parsers:schedule-list"),
{"source_key": "industrial", "every": 1, "period": "days"},
format="json",
)
schedule_id = create_response.data["data"]["id"]
update_response = self.client.patch(
reverse("api_v1:parsers:schedule-detail", kwargs={"pk": schedule_id}),
{"enabled": False},
format="json",
)
self.assertEqual(update_response.status_code, status.HTTP_200_OK)
self.assertFalse(update_response.data["data"]["enabled"])
delete_response = self.client.delete(
reverse("api_v1:parsers:schedule-detail", kwargs={"pk": schedule_id})
)
self.assertEqual(delete_response.status_code, status.HTTP_200_OK)
self.assertFalse(PeriodicTask.objects.filter(pk=schedule_id).exists())