feat: add demo report import workflow
Some checks failed
CI/CD Pipeline / Code Quality Checks (push) Failing after 2m44s
CI/CD Pipeline / Run Tests (push) Successful in 2m50s
CI/CD Pipeline / Build and Push Dev Images (push) Has been skipped
CI/CD Pipeline / Deploy Dev via Compose (push) Has been skipped

This commit is contained in:
2026-07-16 12:59:23 +02:00
parent 46c023f75a
commit 0c052c5a53
58 changed files with 968 additions and 10 deletions

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

View File

@@ -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)