feat: add demo report import workflow
This commit is contained in:
@@ -371,6 +371,7 @@ class ExchangePackageApiTest(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse("api_v1:exchange:package-upload")
|
||||
self.latest_url = reverse("api_v1:exchange:package-latest")
|
||||
password = get_random_string(16)
|
||||
self.user = User.objects.create_user(
|
||||
username="exchange-admin",
|
||||
@@ -378,6 +379,55 @@ class ExchangePackageApiTest(APITestCase):
|
||||
password=password,
|
||||
)
|
||||
|
||||
def test_latest_requires_authentication(self):
|
||||
response = self.client.get(self.latest_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_latest_returns_null_when_no_packages_exist(self):
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
response = self.client.get(self.latest_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
{"success": True, "data": None, "errors": None, "meta": None},
|
||||
)
|
||||
|
||||
def test_latest_returns_newest_package_metadata(self):
|
||||
older = ExchangePackageImport.objects.create(
|
||||
package_id="older-package",
|
||||
package_name="older.zip",
|
||||
package_hash="a" * 64,
|
||||
source_system="mostovik",
|
||||
schema_version=1,
|
||||
delivery_channel=ExchangeDeliveryChannel.API,
|
||||
status="failed",
|
||||
)
|
||||
latest = ExchangePackageImport.objects.create(
|
||||
package_id="latest-package",
|
||||
package_name="latest.zip",
|
||||
package_hash="b" * 64,
|
||||
source_system="mostovik",
|
||||
schema_version=1,
|
||||
delivery_channel=ExchangeDeliveryChannel.API,
|
||||
status="success",
|
||||
)
|
||||
ExchangePackageImport.objects.filter(pk=older.pk).update(
|
||||
created_at=older.created_at.replace(year=older.created_at.year - 1)
|
||||
)
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
response = self.client.get(self.latest_url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["data"]["id"], str(latest.id))
|
||||
self.assertEqual(response.data["data"]["package_id"], "latest-package")
|
||||
self.assertEqual(response.data["data"]["package_name"], "latest.zip")
|
||||
self.assertEqual(response.data["data"]["status"], "success")
|
||||
self.assertIn("created_at", response.data["data"])
|
||||
|
||||
def test_upload_imports_package_and_upserts_models(self):
|
||||
Organization.objects.create(
|
||||
inn="7707083893",
|
||||
|
||||
103
tests/apps/forms/test_report_uploads_api.py
Normal file
103
tests/apps/forms/test_report_uploads_api.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Tests for persistent original report uploads and binary downloads."""
|
||||
|
||||
import hashlib
|
||||
import tempfile
|
||||
|
||||
from apps.core.models import ReportUpload, ReportUploadStatus
|
||||
from apps.core.report_uploads import ReportUploadService
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
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.user.factories import UserFactory
|
||||
|
||||
|
||||
class ReportUploadApiTest(APITestCase):
|
||||
def setUp(self):
|
||||
self.media_directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.media_directory.cleanup)
|
||||
self.override = override_settings(MEDIA_ROOT=self.media_directory.name)
|
||||
self.override.enable()
|
||||
self.addCleanup(self.override.disable)
|
||||
self.user = UserFactory.create_user()
|
||||
|
||||
def _create_upload(self, *, content=b"original excel bytes") -> ReportUpload:
|
||||
return ReportUploadService.create(
|
||||
uploaded_file=SimpleUploadedFile(
|
||||
"Ф-1 отчет.xlsx",
|
||||
content,
|
||||
content_type="application/octet-stream",
|
||||
),
|
||||
form="f1",
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
def test_service_persists_original_bytes_and_finalizes_batch(self):
|
||||
content = b"same bytes that arrived"
|
||||
report_upload = self._create_upload(content=content)
|
||||
|
||||
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
|
||||
report_upload.refresh_from_db()
|
||||
|
||||
self.assertEqual(report_upload.load_batch, 42)
|
||||
self.assertEqual(report_upload.status, ReportUploadStatus.SUCCESS)
|
||||
self.assertEqual(report_upload.file_hash, hashlib.sha256(content).hexdigest())
|
||||
self.assertEqual(report_upload.original_file.read(), content)
|
||||
self.assertEqual(report_upload.file_name, "Ф-1 отчет.xlsx")
|
||||
self.assertEqual(report_upload.uploaded_by, self.user)
|
||||
|
||||
def test_download_requires_authentication(self):
|
||||
report_upload = self._create_upload()
|
||||
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v1:report-upload-download", args=["f1", 42])
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
def test_download_streams_original_bytes_and_filename(self):
|
||||
content = b"unchanged original xlsx bytes"
|
||||
report_upload = self._create_upload(content=content)
|
||||
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v1:report-upload-download", args=["f1", 42])
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(b"".join(response.streaming_content), content)
|
||||
self.assertEqual(
|
||||
response["Content-Type"],
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
self.assertIn("attachment", response["Content-Disposition"])
|
||||
self.assertIn("utf-8", response["Content-Disposition"].lower())
|
||||
|
||||
def test_download_rejects_a_different_non_staff_user(self):
|
||||
report_upload = self._create_upload()
|
||||
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
|
||||
self.client.force_authenticate(UserFactory.create_user())
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v1:report-upload-download", args=["f1", 42])
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.data["errors"][0]["code"], "upload_forbidden")
|
||||
|
||||
def test_download_returns_gone_when_storage_object_was_removed(self):
|
||||
report_upload = self._create_upload()
|
||||
ReportUploadService.finalize(report_upload.id, {"batch_id": 42})
|
||||
report_upload.original_file.storage.delete(report_upload.original_file.name)
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v1:report-upload-download", args=["f1", 42])
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_410_GONE)
|
||||
self.assertEqual(response.data["errors"][0]["code"], "source_file_gone")
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from apps.core.models import ReportUpload, ReportUploadStatus
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
@@ -89,6 +91,11 @@ class FormUploadContractsApiTest(APITestCase):
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.media_directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.media_directory.cleanup)
|
||||
self.media_override = override_settings(MEDIA_ROOT=self.media_directory.name)
|
||||
self.media_override.enable()
|
||||
self.addCleanup(self.media_override.disable)
|
||||
self.user = UserFactory.create_user()
|
||||
self.client.force_authenticate(self.user)
|
||||
|
||||
@@ -172,6 +179,23 @@ class FormUploadContractsApiTest(APITestCase):
|
||||
)
|
||||
parse_mock.assert_not_called()
|
||||
task_mock.delay.assert_called_once()
|
||||
task_args, task_kwargs = task_mock.delay.call_args
|
||||
self.assertIn(
|
||||
response.data["upload_id"],
|
||||
(*task_args, *task_kwargs.values()),
|
||||
)
|
||||
report_upload = ReportUpload.objects.get(
|
||||
pk=response.data["upload_id"]
|
||||
)
|
||||
self.assertEqual(report_upload.form, case["form"])
|
||||
self.assertEqual(
|
||||
report_upload.status, ReportUploadStatus.QUEUED
|
||||
)
|
||||
self.assertIsNone(report_upload.load_batch)
|
||||
self.assertEqual(
|
||||
report_upload.original_file.read(),
|
||||
b"0" * self.BACKGROUND_THRESHOLD_PLUS,
|
||||
)
|
||||
|
||||
def test_upload_sync_contract(self):
|
||||
for _, case in self.CASES.items():
|
||||
@@ -201,6 +225,13 @@ class FormUploadContractsApiTest(APITestCase):
|
||||
)
|
||||
parse_mock.assert_called_once()
|
||||
task_mock.delay.assert_not_called()
|
||||
report_upload = ReportUpload.objects.get(
|
||||
pk=response.data["upload_id"]
|
||||
)
|
||||
self.assertEqual(report_upload.form, case["form"])
|
||||
self.assertEqual(report_upload.load_batch, 101)
|
||||
self.assertEqual(report_upload.status, ReportUploadStatus.SUCCESS)
|
||||
self.assertEqual(report_upload.original_file.read(), b"0" * 256)
|
||||
|
||||
def test_upload_processing_error_contract(self):
|
||||
for _, case in self.CASES.items():
|
||||
@@ -219,3 +250,7 @@ class FormUploadContractsApiTest(APITestCase):
|
||||
self.assertEqual(response.data["error_message"], "parse failed")
|
||||
self.assertEqual(response.data["details"], [])
|
||||
parse_mock.assert_called_once()
|
||||
report_upload = ReportUpload.objects.filter(form=case["form"]).latest(
|
||||
"created_at"
|
||||
)
|
||||
self.assertEqual(report_upload.status, ReportUploadStatus.FAILED)
|
||||
|
||||
107
tests/apps/organization/test_delete_test_companies_command.py
Normal file
107
tests/apps/organization/test_delete_test_companies_command.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for deleting the fixed Mostovik demonstration companies."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
|
||||
from apps.core.models import ReportUpload
|
||||
from apps.core.report_uploads import ReportUploadService
|
||||
from apps.external_data.models import IndustrialProduct
|
||||
from apps.form_1.models import FormF1Record
|
||||
from apps.organization.models import Organization
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class DeleteTestCompaniesCommandTest(TestCase):
|
||||
"""The cleanup command must be narrow, cascading, and idempotent."""
|
||||
|
||||
def test_deletes_only_exact_test_identities_with_related_data(self):
|
||||
first = Organization.objects.create(
|
||||
name="Тестовая компания 1",
|
||||
inn="7709000010",
|
||||
)
|
||||
second = Organization.objects.create(
|
||||
name="Тестовая компания 2",
|
||||
inn="7709000028",
|
||||
)
|
||||
same_pattern_but_not_test_identity = Organization.objects.create(
|
||||
name="Тестовая компания 99",
|
||||
inn="7709000099",
|
||||
)
|
||||
same_inn_pattern_but_real_name = Organization.objects.create(
|
||||
name="Производственная компания",
|
||||
inn="7709000035",
|
||||
)
|
||||
FormF1Record.objects.create(
|
||||
organization=first,
|
||||
load_batch=1,
|
||||
report_year=2025,
|
||||
report_month=9,
|
||||
)
|
||||
IndustrialProduct.objects.create(
|
||||
organization=second,
|
||||
product_name="Тестовый комплекс",
|
||||
product_class="Оборудование",
|
||||
)
|
||||
demo_upload = ReportUploadService.create(
|
||||
uploaded_file=SimpleUploadedFile("Ф-1_2025-09.xlsx", b"demo"),
|
||||
form="f1",
|
||||
)
|
||||
demo_file_name = demo_upload.original_file.name
|
||||
demo_storage = demo_upload.original_file.storage
|
||||
unrelated_upload = ReportUploadService.create(
|
||||
uploaded_file=SimpleUploadedFile("Ф-1.xlsx", b"unrelated"),
|
||||
form="f1",
|
||||
)
|
||||
|
||||
stdout = StringIO()
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
result = call_command("delete_test_companies", stdout=stdout)
|
||||
payload = json.loads(result)
|
||||
|
||||
self.assertEqual(payload["organizations_deleted"], 2)
|
||||
self.assertEqual(payload["related_objects_deleted"], 2)
|
||||
self.assertEqual(payload["report_uploads_deleted"], 1)
|
||||
self.assertFalse(Organization.objects.filter(pk__in=[first.pk, second.pk]))
|
||||
self.assertTrue(
|
||||
Organization.objects.filter(pk=same_pattern_but_not_test_identity.pk)
|
||||
)
|
||||
self.assertTrue(
|
||||
Organization.objects.filter(pk=same_inn_pattern_but_real_name.pk)
|
||||
)
|
||||
self.assertFalse(FormF1Record.objects.exists())
|
||||
self.assertFalse(IndustrialProduct.objects.exists())
|
||||
self.assertFalse(ReportUpload.objects.filter(pk=demo_upload.pk).exists())
|
||||
self.assertFalse(demo_storage.exists(demo_file_name))
|
||||
self.assertTrue(ReportUpload.objects.filter(pk=unrelated_upload.pk).exists())
|
||||
|
||||
repeated = json.loads(
|
||||
call_command("delete_test_companies", stdout=StringIO())
|
||||
)
|
||||
self.assertEqual(repeated["organizations_deleted"], 0)
|
||||
self.assertEqual(repeated["related_objects_deleted"], 0)
|
||||
self.assertEqual(repeated["report_uploads_deleted"], 0)
|
||||
|
||||
def test_dry_run_preserves_test_companies(self):
|
||||
organization = Organization.objects.create(
|
||||
name="Тестовая компания 1",
|
||||
inn="7709000010",
|
||||
)
|
||||
demo_upload = ReportUploadService.create(
|
||||
uploaded_file=SimpleUploadedFile("Ф-1_2025-09.xlsx", b"demo"),
|
||||
form="f1",
|
||||
)
|
||||
|
||||
result = call_command(
|
||||
"delete_test_companies",
|
||||
dry_run=True,
|
||||
stdout=StringIO(),
|
||||
)
|
||||
|
||||
self.assertTrue(Organization.objects.filter(pk=organization.pk).exists())
|
||||
self.assertTrue(ReportUpload.objects.filter(pk=demo_upload.pk).exists())
|
||||
self.assertTrue(demo_upload.original_file.storage.exists(demo_upload.original_file.name))
|
||||
self.assertTrue(json.loads(result)["dry_run"])
|
||||
Reference in New Issue
Block a user