mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
File Manager: external link, notes and photos on library files (#3128)
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"""Integration tests for the client-rendered preview thumbnail upload (#2976).
|
||||
|
||||
STEP/PDF/spreadsheet previews render in the browser and post their first
|
||||
render to POST /library/files/{id}/preview-thumbnail. These tests pin the
|
||||
endpoint's contract: PNG-only, capped size, only for the client-preview file
|
||||
types, and never replacing an existing thumbnail.
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from PIL import Image
|
||||
|
||||
from backend.app.core.config import settings as app_settings
|
||||
from backend.app.models.library import LibraryFile
|
||||
|
||||
|
||||
def _png_bytes(size: tuple[int, int] = (300, 300), color: str = "red") -> bytes:
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", size, color).save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_storage(monkeypatch, tmp_path):
|
||||
"""Point thumbnail storage at a throwaway directory."""
|
||||
monkeypatch.setattr(app_settings, "base_dir", tmp_path)
|
||||
monkeypatch.setattr(app_settings, "archive_dir", tmp_path / "archive")
|
||||
return tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def file_factory(db_session):
|
||||
"""Factory for LibraryFile rows of arbitrary file_type."""
|
||||
_counter = [0]
|
||||
|
||||
async def _create_file(**kwargs):
|
||||
_counter[0] += 1
|
||||
counter = _counter[0]
|
||||
defaults = {
|
||||
"filename": f"part{counter}.step",
|
||||
"file_path": f"library/files/part{counter}.step",
|
||||
"file_type": "step",
|
||||
"file_size": 100,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
library_file = LibraryFile(**defaults)
|
||||
db_session.add(library_file)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(library_file)
|
||||
return library_file
|
||||
|
||||
return _create_file
|
||||
|
||||
|
||||
class TestPreviewThumbnailUpload:
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_sets_thumbnail_path(
|
||||
self, async_client: AsyncClient, db_session, file_factory, isolated_storage
|
||||
):
|
||||
library_file = await file_factory(file_type="step")
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", _png_bytes(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"updated": True}
|
||||
|
||||
await db_session.refresh(library_file)
|
||||
assert library_file.thumbnail_path
|
||||
stored = isolated_storage / library_file.thumbnail_path
|
||||
assert stored.exists()
|
||||
with Image.open(stored) as img:
|
||||
assert img.format == "PNG"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_downscales_oversized_image(
|
||||
self, async_client: AsyncClient, db_session, file_factory, isolated_storage
|
||||
):
|
||||
library_file = await file_factory(file_type="pdf", filename="doc.pdf", file_path="library/files/doc.pdf")
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", _png_bytes(size=(1024, 1024)), "image/png")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
await db_session.refresh(library_file)
|
||||
with Image.open(isolated_storage / library_file.thumbnail_path) as img:
|
||||
assert max(img.size) <= 512
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_skips_when_thumbnail_exists(
|
||||
self, async_client: AsyncClient, db_session, file_factory, isolated_storage
|
||||
):
|
||||
library_file = await file_factory(file_type="csv", thumbnail_path="archive/library/thumbnails/existing.png")
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", _png_bytes(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"updated": False}
|
||||
|
||||
await db_session.refresh(library_file)
|
||||
assert library_file.thumbnail_path == "archive/library/thumbnails/existing.png"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_rejected_for_server_rendered_types(
|
||||
self, async_client: AsyncClient, file_factory, isolated_storage
|
||||
):
|
||||
# STL thumbnails are generated server-side; the client route must not
|
||||
# be able to overwrite them.
|
||||
library_file = await file_factory(file_type="stl", filename="part.stl")
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", _png_bytes(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_rejects_non_png(self, async_client: AsyncClient, file_factory, isolated_storage):
|
||||
library_file = await file_factory(file_type="step")
|
||||
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", (64, 64), "blue").save(buf, "JPEG")
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", buf.getvalue(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_rejects_garbage_bytes(self, async_client: AsyncClient, file_factory, isolated_storage):
|
||||
library_file = await file_factory(file_type="xlsx")
|
||||
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", b"not an image at all", "image/png")},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_rejects_oversized_payload(self, async_client: AsyncClient, file_factory, isolated_storage):
|
||||
library_file = await file_factory(file_type="ods")
|
||||
|
||||
oversized = b"\x89PNG\r\n\x1a\n" + b"\x00" * (2 * 1024 * 1024)
|
||||
response = await async_client.post(
|
||||
f"/api/v1/library/files/{library_file.id}/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", oversized, "image/png")},
|
||||
)
|
||||
assert response.status_code == 413
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_upload_missing_file_returns_404(self, async_client: AsyncClient, isolated_storage):
|
||||
response = await async_client.post(
|
||||
"/api/v1/library/files/999999/preview-thumbnail",
|
||||
files={"thumbnail": ("preview.png", _png_bytes(), "image/png")},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user