feat(parsers): support FNS zip uploads in admin
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
"""Tests for parsers admin configurations."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from apps.parsers.admin import (
|
||||
FinancialReportAdmin,
|
||||
HasCertificateNumberFilter,
|
||||
@@ -22,7 +27,9 @@ from apps.parsers.models import (
|
||||
)
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django.contrib.messages.storage.fallback import FallbackStorage
|
||||
from django.test import RequestFactory, TestCase
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import RequestFactory, TestCase, override_settings
|
||||
from openpyxl import Workbook
|
||||
|
||||
from tests.apps.parsers.factories import (
|
||||
IndustrialCertificateRecordFactory,
|
||||
@@ -42,6 +49,41 @@ def _digits(length: int) -> str:
|
||||
return "".join(str(fake.random_int(0, 9)) for _ in range(length))
|
||||
|
||||
|
||||
def _build_fns_excel_bytes() -> bytes:
|
||||
workbook = Workbook()
|
||||
worksheet = workbook.active
|
||||
year = fake.random_int(min=2020, max=2025)
|
||||
worksheet.append(["Форма №1", None, year, None])
|
||||
worksheet.append([None, "Код", "Начало", "Конец"])
|
||||
worksheet.append(
|
||||
[
|
||||
fake.word(),
|
||||
_digits(4),
|
||||
fake.random_int(min=10, max=999),
|
||||
fake.random_int(min=10, max=999),
|
||||
]
|
||||
)
|
||||
buffer = io.BytesIO()
|
||||
workbook.save(buffer)
|
||||
workbook.close()
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _build_fns_zip_upload() -> SimpleUploadedFile:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
archive.writestr(
|
||||
f"fin_{_digits(5)}_{_digits(13)}.xlsx",
|
||||
_build_fns_excel_bytes(),
|
||||
)
|
||||
archive.writestr("ignored.txt", b"invalid")
|
||||
return SimpleUploadedFile(
|
||||
"fin_ropk.zip",
|
||||
buffer.getvalue(),
|
||||
content_type="application/zip",
|
||||
)
|
||||
|
||||
|
||||
class ParsersAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
@@ -55,6 +97,13 @@ class ParsersAdminTest(TestCase):
|
||||
request._messages = FallbackStorage(request)
|
||||
return request
|
||||
|
||||
def _post_request(self, path, data):
|
||||
request = self.factory.post(path, data=data)
|
||||
request.user = self.user
|
||||
request.session = {}
|
||||
request._messages = FallbackStorage(request)
|
||||
return request
|
||||
|
||||
def test_proxy_admin_actions(self):
|
||||
admin = ProxyAdmin(Proxy, self.site)
|
||||
proxy = ProxyFactory(is_active=False, fail_count=5)
|
||||
@@ -248,3 +297,23 @@ class ParsersAdminTest(TestCase):
|
||||
self.assertIn("registry_organization__pn_name", admin.search_fields)
|
||||
route_names = [route.name for route in admin.get_urls()]
|
||||
self.assertIn("parsers_financialreport_upload_excel", route_names)
|
||||
self.assertIn("parsers_financialreport_upload_zip", route_names)
|
||||
|
||||
def test_financial_report_admin_upload_zip_view(self):
|
||||
admin = FinancialReportAdmin(FinancialReport, self.site)
|
||||
archive_upload = _build_fns_zip_upload()
|
||||
request = self._post_request(
|
||||
"/admin/parsers/financialreport/upload-zip/",
|
||||
{"file": archive_upload},
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir, override_settings(
|
||||
FNS_WATCH_DIRECTORY=os.path.join(tmpdir, "watch"),
|
||||
FNS_PROCESSED_DIRECTORY=os.path.join(tmpdir, "processed"),
|
||||
FNS_FAILED_DIRECTORY=os.path.join(tmpdir, "failed"),
|
||||
):
|
||||
response = admin.upload_zip_view(request)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(FinancialReport.objects.count(), 1)
|
||||
self.assertEqual(FinancialReportLine.objects.count(), 1)
|
||||
|
||||
@@ -4,9 +4,11 @@ import io
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from apps.core.models import BackgroundJob
|
||||
from apps.parsers.fns_upload import FNSUploadService
|
||||
from apps.parsers.models import FinancialReport, FinancialReportLine
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import override_settings
|
||||
@@ -38,6 +40,14 @@ def _build_fns_excel_bytes() -> bytes:
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _build_fns_zip_bytes(file_map: dict[str, bytes]) -> bytes:
|
||||
buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
for file_name, content in file_map.items():
|
||||
archive.writestr(file_name, content)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class FNSUploadIntegrationTest(APITestCase):
|
||||
"""Tests real upload + processing of FNS files."""
|
||||
|
||||
@@ -348,7 +358,7 @@ class FNSUploadIntegrationTest(APITestCase):
|
||||
FNS_WATCH_DIRECTORY=watch_dir,
|
||||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||||
FNS_FAILED_DIRECTORY=failed_dir,
|
||||
), patch("apps.parsers.views.Path.touch", side_effect=FileExistsError):
|
||||
), patch("apps.parsers.fns_upload.Path.touch", side_effect=FileExistsError):
|
||||
response = self.client.post(
|
||||
self.upload_url,
|
||||
{"files": [upload]},
|
||||
@@ -375,7 +385,10 @@ class FNSUploadIntegrationTest(APITestCase):
|
||||
FNS_WATCH_DIRECTORY=watch_dir,
|
||||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||||
FNS_FAILED_DIRECTORY=failed_dir,
|
||||
), patch("apps.parsers.views.open", side_effect=OSError("disk full")):
|
||||
), patch(
|
||||
"apps.parsers.fns_upload.Path.write_bytes",
|
||||
side_effect=OSError("disk full"),
|
||||
):
|
||||
response = self.client.post(
|
||||
self.upload_url,
|
||||
{"files": [upload]},
|
||||
@@ -406,9 +419,9 @@ class FNSUploadIntegrationTest(APITestCase):
|
||||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||||
FNS_FAILED_DIRECTORY=failed_dir,
|
||||
), patch(
|
||||
"apps.parsers.views.uuid.uuid4", return_value="job-task-id"
|
||||
"apps.parsers.fns_upload.uuid.uuid4", return_value="job-task-id"
|
||||
), patch(
|
||||
"apps.parsers.views.process_fns_file.apply_async",
|
||||
"apps.parsers.fns_upload.process_fns_file.apply_async",
|
||||
side_effect=RuntimeError("queue down"),
|
||||
):
|
||||
response = self.client.post(
|
||||
@@ -426,3 +439,54 @@ class FNSUploadIntegrationTest(APITestCase):
|
||||
self.assertFalse(
|
||||
os.path.exists(os.path.join(watch_dir, f"{filename}.lock"))
|
||||
)
|
||||
|
||||
def test_queue_zip_archive_processes_valid_files_and_skips_invalid(self):
|
||||
first_name = f"fin_{_digits(5)}_{_digits(13)}.xlsx"
|
||||
second_name = f"fin_{_digits(5)}_{_digits(13)}.xlsx"
|
||||
zip_content = _build_fns_zip_bytes(
|
||||
{
|
||||
first_name: _build_fns_excel_bytes(),
|
||||
second_name: _build_fns_excel_bytes(),
|
||||
"nested/fin_0000001_1234567890123.xlsx": _build_fns_excel_bytes(),
|
||||
"readme.txt": b"invalid",
|
||||
}
|
||||
)
|
||||
archive_upload = SimpleUploadedFile(
|
||||
"fin_ropk.zip",
|
||||
zip_content,
|
||||
content_type="application/zip",
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
watch_dir, processed_dir, failed_dir = self._dirs(tmpdir)
|
||||
with override_settings(
|
||||
FNS_WATCH_DIRECTORY=watch_dir,
|
||||
FNS_PROCESSED_DIRECTORY=processed_dir,
|
||||
FNS_FAILED_DIRECTORY=failed_dir,
|
||||
):
|
||||
result = FNSUploadService.queue_zip_archive(
|
||||
archive_file=archive_upload,
|
||||
requested_by_id=self.admin.id,
|
||||
)
|
||||
|
||||
self.assertEqual(result.queued, 2)
|
||||
self.assertEqual(result.skipped, 0)
|
||||
self.assertEqual(result.invalid, 2)
|
||||
self.assertEqual(FinancialReport.objects.count(), 2)
|
||||
self.assertEqual(FinancialReportLine.objects.count(), 2)
|
||||
|
||||
def test_queue_zip_archive_rejects_bad_zip(self):
|
||||
archive_upload = SimpleUploadedFile(
|
||||
"fin_ropk.zip",
|
||||
b"not-a-zip",
|
||||
content_type="application/zip",
|
||||
)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
ValueError,
|
||||
"Загруженный файл не является корректным ZIP архивом",
|
||||
):
|
||||
FNSUploadService.queue_zip_archive(
|
||||
archive_file=archive_upload,
|
||||
requested_by_id=self.admin.id,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user