File di riferimento rimossi con il task, immagini animate rifiutate, filtro production_run_id (#43)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""#43: reference files removed with their task, animated images refused,
|
||||
GET /api/measurements filtered by production run."""
|
||||
import io
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
from sqlalchemy import select
|
||||
|
||||
from src.backend.config import settings
|
||||
from src.backend.models.orm.measurement import Measurement
|
||||
from src.backend.models.orm.vision import VisionReferenceImage
|
||||
from src.backend.tests.conftest import auth_headers, create_test_recipe
|
||||
from src.backend.tests.test_vision_upload_limits import a_png, recipe_parts
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def redirect_uploads(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(settings, "upload_dir", str(tmp_path))
|
||||
|
||||
|
||||
async def _upload_reference(client, user, task_id):
|
||||
resp = await client.post(
|
||||
"/api/vision/reference-images", headers=auth_headers(user),
|
||||
files={"image": ("p.png", a_png(), "image/png")},
|
||||
data={"task_id": str(task_id)},
|
||||
)
|
||||
assert resp.status_code in (200, 201), resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_task_removes_the_reference_file(client, db_session, maker_user):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
_, task, _ = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
await _upload_reference(client, maker_user, task.id)
|
||||
ref = (await db_session.execute(select(VisionReferenceImage))).scalars().one()
|
||||
path = settings.upload_path / ref.path
|
||||
assert path.exists()
|
||||
|
||||
resp = await client.delete(f"/api/tasks/{task.id}", headers=auth_headers(maker_user))
|
||||
|
||||
assert resp.status_code == 204
|
||||
assert not path.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_task_keeps_a_file_another_task_still_references(
|
||||
client, db_session, maker_user,
|
||||
):
|
||||
r1 = await create_test_recipe(db_session, maker_user.id)
|
||||
r2 = await create_test_recipe(db_session, maker_user.id, code="REC-002")
|
||||
_, t1, _ = await recipe_parts(db_session, r1.id)
|
||||
_, t2, _ = await recipe_parts(db_session, r2.id)
|
||||
await db_session.commit()
|
||||
await _upload_reference(client, maker_user, t1.id)
|
||||
ref = (await db_session.execute(select(VisionReferenceImage))).scalars().one()
|
||||
db_session.add(VisionReferenceImage(task_id=t2.id, path=ref.path))
|
||||
await db_session.commit()
|
||||
path = settings.upload_path / ref.path
|
||||
|
||||
resp = await client.delete(f"/api/tasks/{t1.id}", headers=auth_headers(maker_user))
|
||||
|
||||
assert resp.status_code == 204
|
||||
assert path.exists()
|
||||
|
||||
|
||||
def _animated(fmt: str, frames: int) -> bytes:
|
||||
imgs = [Image.new("L", (16, 16), color=i * 40) for i in range(frames)]
|
||||
buf = io.BytesIO()
|
||||
imgs[0].save(buf, format=fmt, save_all=True, append_images=imgs[1:], duration=50)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("fmt,mime,frames,expected", [
|
||||
("GIF", "image/gif", 2, 422),
|
||||
("WEBP", "image/webp", 2, 422),
|
||||
("GIF", "image/gif", 1, 201),
|
||||
])
|
||||
async def test_animated_images_are_refused(
|
||||
client, db_session, maker_user, fmt, mime, frames, expected,
|
||||
):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
_, task, _ = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
|
||||
resp = await client.post(
|
||||
"/api/vision/reference-images", headers=auth_headers(maker_user),
|
||||
files={"image": ("a", _animated(fmt, frames), mime)},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
|
||||
if expected == 422:
|
||||
assert resp.status_code == 422
|
||||
assert "animated" in resp.json()["detail"]
|
||||
else:
|
||||
assert resp.status_code in (200, 201), resp.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_measurements_filter_by_production_run(client, db_session, maker_user):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
version, _, subtask = await recipe_parts(db_session, recipe.id)
|
||||
for run in (7, 7, 8, None):
|
||||
db_session.add(Measurement(
|
||||
subtask_id=subtask.id, version_id=version.id, measured_by=maker_user.id,
|
||||
value=1.0, pass_fail="pass", deviation=0.0, input_method="manual",
|
||||
production_run_id=run,
|
||||
))
|
||||
await db_session.commit()
|
||||
|
||||
resp = await client.get(
|
||||
"/api/measurements/", headers=auth_headers(maker_user),
|
||||
params={"production_run_id": 7},
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total"] == 2
|
||||
assert {m["production_run_id"] for m in body["items"]} == {7}
|
||||
Reference in New Issue
Block a user