185 lines
8.2 KiB
Python
185 lines
8.2 KiB
Python
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()
|