import hashlib from unittest import mock import pytest from django.core.files.uploadedfile import SimpleUploadedFile from infrasynth.files.models import FileCategory, StoredFile from infrasynth.files.services import FileService from infrasynth.files.signals import file_deleted, file_uploaded pytestmark = pytest.mark.django_db def _make_pdf(name="doc.pdf", content=b"%PDF-1.4 test content"): return SimpleUploadedFile(name, content, content_type="application/pdf") @pytest.fixture def upload_signals(): sent = [] receiver = lambda **kw: sent.append(kw) # noqa: E731 file_uploaded.connect(receiver, weak=False) yield sent file_uploaded.disconnect(receiver) class TestUpload: def test_upload_creates_stored_file(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) assert StoredFile.objects.filter(pk=stored.id).exists() assert stored.original_filename == "doc.pdf" assert stored.mime_type == "application/pdf" assert stored.size_bytes == len(b"%PDF-1.4 test content") assert stored.storage_backend == "local" assert stored.storage_key.startswith(stored.storage_key.split("/")[0] + "/") assert stored.storage_key.endswith("doc.pdf") assert stored.checksum_sha256 == hashlib.sha256(b"%PDF-1.4 test content").hexdigest() def test_upload_with_category_uses_storage_path(self, user, media_root): category = FileCategory.objects.create(slug="contracts", name="Contracts", storage_path="contracts") stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user, category_slug="contracts") assert stored.category_id == category.slug assert stored.storage_key.startswith("contracts/") def test_upload_rejects_invalid_extension(self, user, media_root): FileCategory.objects.create( slug="pdfs", name="PDFs", storage_path="pdfs", allowed_extensions="pdf", ) with pytest.raises(ValueError, match="not allowed"): FileService().upload(_make_pdf("image.png"), filename="image.png", category_slug="pdfs") def test_upload_rejects_oversized_file(self, user, media_root): FileCategory.objects.create( slug="small", name="Small", storage_path="small", allowed_extensions="pdf", max_size_bytes=10, ) with pytest.raises(ValueError, match="exceeds max size"): FileService().upload(_make_pdf(), filename="doc.pdf", category_slug="small") def test_upload_inactive_category_rejected(self, user, media_root): FileCategory.objects.create(slug="inactive", name="Inactive", storage_path="x", is_active=False) with pytest.raises(ValueError, match="not found or inactive"): FileService().upload(_make_pdf(), filename="doc.pdf", category_slug="inactive") def test_upload_emits_signal(self, user, media_root, upload_signals): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) assert upload_signals assert upload_signals[0]["file_id"] == stored.id assert upload_signals[0]["filename"] == "doc.pdf" def test_upload_file_is_stored_on_disk(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) path = media_root / stored.storage_key assert path.exists() assert path.read_bytes() == b"%PDF-1.4 test content" class TestSignedUrl: def test_generates_url(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) url = FileService().get_signed_url(stored.id) assert "media" in url assert stored.storage_key.split("/")[-1] in url class TestDownload: def test_local_file_returns_file_response(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) request = type("R", (), {})() response = FileService().get_download_response(stored, request) assert response.status_code == 200 assert "attachment" in response["Content-Disposition"] body = b"".join(response.streaming_content) assert body == b"%PDF-1.4 test content" def test_non_local_redirects_to_signed_url(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) stored.storage_backend = "S3" stored.save(update_fields=["storage_backend"]) with mock.patch( "infrasynth.files.services.get_storage_backend", return_value=mock.Mock(generate_signed_url=lambda key, exp: "https://signed/url"), ): request = type("R", (), {})() response = FileService().get_download_response(stored, request) assert response.status_code == 302 assert response.url == "https://signed/url" def test_x_sendfile_enabled(self, user, media_root, settings): settings.INFRASYNTH_FILES = { **settings.INFRASYNTH_FILES, "ENABLE_X_SENDFILE": True, } stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) request = type("R", (), {})() response = FileService().get_download_response(stored, request) assert response["X-Sendfile"] == stored.storage_key class TestDelete: def test_soft_delete_marks_metadata(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) assert FileService().delete(stored, soft=True) is True stored.refresh_from_db() assert stored.metadata.get("is_deleted") is True assert StoredFile.objects.filter(pk=stored.pk).exists() def test_hard_delete_removes_row_and_file(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) path = media_root / stored.storage_key assert path.exists() assert FileService().delete(stored, soft=False) is True assert not StoredFile.objects.filter(pk=stored.pk).exists() assert not path.exists() def test_delete_emits_signal(self, user, media_root): sent = [] receiver = lambda **kw: sent.append(kw) # noqa: E731 file_deleted.connect(receiver, weak=False) stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user) FileService().delete(stored, soft=False) assert sent assert sent[0]["file_id"] == stored.id file_deleted.disconnect(receiver) class TestGetFileInfo: def test_returns_full_metadata(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user, is_public=True) info = FileService().get_file_info(stored) assert info["id"] == stored.id assert info["original_filename"] == "doc.pdf" assert info["is_public"] is True assert info["uploaded_by"]["id"] == user.id assert info["category"] is None assert info["created_at"] def test_info_with_category(self, user, media_root): FileCategory.objects.create(slug="docs", name="Docs", storage_path="docs") stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user, category_slug="docs") info = FileService().get_file_info(stored) assert info["category"] == {"slug": "docs", "name": "Docs"} class TestPipelineScheduling: def test_schedules_pipeline_execution(self, user, media_root): from infrasynth.files.models import PipelineExecution, ProcessingPipeline pipeline = ProcessingPipeline.objects.create(name="Noop", slug="noop", steps=[]) stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user, pipeline_slug="noop") execution = PipelineExecution.objects.get(file=stored) assert execution.pipeline_id == pipeline.id assert execution.status == PipelineExecution.Status.COMPLETED def test_unknown_pipeline_ignored(self, user, media_root): stored = FileService().upload(_make_pdf(), filename="doc.pdf", user=user, pipeline_slug="does-not-exist") from infrasynth.files.models import PipelineExecution assert not PipelineExecution.objects.filter(file=stored).exists()