"""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())