"""Tests for frontend source cards API.""" from __future__ import annotations from pathlib import Path from tempfile import TemporaryDirectory from apps.core.models import BackgroundJob, JobStatus from apps.parsers.models import FinancialReport, FinancialReportLine, ParserLoadLog from django.test import override_settings from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase from tests.apps.parsers.factories import ( IndustrialCertificateRecordFactory, IndustrialProductRecordFactory, InspectionRecordFactory, ManufacturerRecordFactory, ParserLoadLogFactory, ) from tests.apps.user.factories import UserFactory from tests.utils.fixtures import fake def _digits(length: int) -> str: return "".join(str(fake.random_int(0, 9)) for _ in range(length)) class SourceCardsApiTestCase(APITestCase): def setUp(self): self.user = UserFactory.create_user() self.admin = UserFactory.create_user(is_staff=True) self.client.force_authenticate(self.user) def test_source_cards_list_returns_aggregated_data(self): report = FinancialReport.objects.create( external_id=_digits(5), ogrn=_digits(13), file_name=f"fin_{_digits(5)}_{_digits(13)}.xlsx", file_hash=fake.sha256(raw_output=False), load_batch=1, status=FinancialReport.Status.SUCCESS, source=FinancialReport.SourceType.API, ) FinancialReportLine.objects.create( report=report, form_code="1", line_code="1100", line_name="Активы", year=2025, period_start=100, period_end=200, ) FinancialReportLine.objects.create( report=report, form_code="2", line_code="2110", line_name="Выручка", year=2025, period_start=300, period_end=400, ) ParserLoadLogFactory( source=ParserLoadLog.Source.FNS_REPORTS, status="success", records_count=2, ) InspectionRecordFactory() BackgroundJob.objects.create( task_id="job-inspections-active", task_name="apps.parsers.tasks.sync_inspections", status=JobStatus.STARTED, progress=63, progress_message="sync", user_id=self.user.id, meta={"source": ParserLoadLog.Source.INSPECTIONS}, ) response = self.client.get(reverse("api_v1:sources:source-cards-list")) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data["success"]) cards = {item["slug"]: item for item in response.data["data"]} self.assertIn("financial-indicators", cards) self.assertIn("planned-inspections", cards) fns_card = cards["financial-indicators"] self.assertEqual(fns_card["records_count"], 2) self.assertEqual(fns_card["organizations_count"], 1) self.assertEqual(fns_card["status"], "success") self.assertFalse(fns_card["refresh_requires_params"]) inspections_card = cards["planned-inspections"] self.assertEqual(inspections_card["status"], "in_progress") self.assertEqual(inspections_card["progress"], 63) def test_source_card_detail_returns_combined_minprom_stats(self): shared_inn = _digits(10) IndustrialCertificateRecordFactory(inn=shared_inn) IndustrialProductRecordFactory(inn=shared_inn) ManufacturerRecordFactory(inn=shared_inn) ParserLoadLogFactory( source=ParserLoadLog.Source.INDUSTRIAL, status="success", records_count=1, ) ParserLoadLogFactory( source=ParserLoadLog.Source.INDUSTRIAL_PRODUCTS, status="success", records_count=1, ) ParserLoadLogFactory( source=ParserLoadLog.Source.MANUFACTURES, status="success", records_count=1, ) response = self.client.get( reverse( "api_v1:sources:source-cards-detail", kwargs={"slug": "manufacturers-and-products"}, ) ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data["success"]) card = response.data["data"] self.assertEqual(card["records_count"], 3) self.assertEqual(card["organizations_count"], 1) self.assertEqual(card["status"], "success") self.assertEqual(len(card["source_items"]), 3) self.assertEqual(card["source_items"][0]["latest_load"]["status"], "success") def test_source_task_statuses_returns_table_rows(self): ParserLoadLogFactory( source=ParserLoadLog.Source.INDUSTRIAL, status="success", records_count=12, ) ParserLoadLogFactory( source=ParserLoadLog.Source.INSPECTIONS, status="failed", records_count=0, ) response = self.client.get(reverse("api_v1:sources:source-cards-statuses")) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertTrue(response.data["success"]) rows = response.data["data"] self.assertGreaterEqual(len(rows), 1) row = rows[0] self.assertIn("row_number", row) self.assertIn("source", row) self.assertIn("actualized_at", row) self.assertIn("next_update_at", row) self.assertIn("records_count", row) self.assertIn("organizations_count", row) self.assertIn("status", row) self.assertIn("status_label", row) self.assertIn("active_tasks", row) def test_refresh_creates_background_job_and_returns_task(self): self.client.force_authenticate(self.admin) with TemporaryDirectory() as tmp_dir, override_settings( FNS_WATCH_DIRECTORY=str(Path(tmp_dir) / "watch"), FNS_PROCESSED_DIRECTORY=str(Path(tmp_dir) / "processed"), FNS_FAILED_DIRECTORY=str(Path(tmp_dir) / "failed"), ): response = self.client.post( reverse( "api_v1:sources:source-cards-refresh", kwargs={"slug": "financial-indicators"}, ), {}, format="json", ) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) self.assertEqual(response.data["status"], "accepted") self.assertEqual(set(response.data.keys()), {"task_id", "status"}) task_id = response.data["task_id"] self.assertTrue( BackgroundJob.objects.filter( task_id=task_id, task_name="apps.parsers.tasks.scan_fns_directory", user_id=self.admin.id, ).exists() ) def test_refresh_procurements_requires_region_code(self): self.client.force_authenticate(self.admin) response = self.client.post( reverse( "api_v1:sources:source-cards-refresh", kwargs={"slug": "public-procurements"}, ), {}, format="json", ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertFalse(response.data["success"]) def test_refresh_forbidden_for_regular_user(self): response = self.client.post( reverse( "api_v1:sources:source-cards-refresh", kwargs={"slug": "financial-indicators"}, ), {}, format="json", ) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)