91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
import pytest
|
|
import io
|
|
from fastapi import status
|
|
from unittest.mock import patch
|
|
|
|
|
|
# Minimal valid 1x1 PNG bytes
|
|
MINIMAL_PNG = (
|
|
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
|
|
b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00'
|
|
b'\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18'
|
|
b'\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
|
|
)
|
|
|
|
|
|
class TestAIExtraction:
|
|
"""Test AI label extraction pipeline (mocked)."""
|
|
|
|
def test_gemini_extraction(self, test_client, user_token, mock_gemini):
|
|
"""Test AI extraction using Gemini."""
|
|
response = test_client.post(
|
|
"/items/extract-label?mode=item",
|
|
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
def test_claude_extraction(self, test_client, user_token, mock_claude):
|
|
"""Test AI extraction using Claude."""
|
|
response = test_client.post(
|
|
"/items/extract-label?mode=item",
|
|
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
def test_extraction_box_mode(self, test_client, user_token, mock_gemini):
|
|
"""Test AI extraction in 'box' mode (focus on labels)."""
|
|
response = test_client.post(
|
|
"/items/extract-label?mode=box",
|
|
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
def test_extraction_invalid_file_type(self, test_client, user_token):
|
|
"""Test that invalid file type is rejected."""
|
|
response = test_client.post(
|
|
"/items/extract-label",
|
|
files={"file": ("test.txt", io.BytesIO(b"not an image"), "text/plain")},
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
|
|
|
def test_extraction_missing_file(self, test_client, user_token):
|
|
"""Test that missing file returns 422."""
|
|
response = test_client.post(
|
|
"/items/extract-label",
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
|
|
class TestAIValidation:
|
|
"""Test AI extraction validation (user confirmation before save)."""
|
|
|
|
def test_extraction_requires_auth(self, test_client):
|
|
"""Test that extraction endpoint requires authentication."""
|
|
response = test_client.post(
|
|
"/items/extract-label",
|
|
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")}
|
|
)
|
|
assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
|
|
|
|
def test_extraction_result_not_auto_saved(self, test_client, test_db, user_token, mock_gemini):
|
|
"""Test that extracted data is returned but not auto-saved to DB."""
|
|
from backend.models import Item
|
|
|
|
initial_count = test_db.query(Item).count()
|
|
|
|
response = test_client.post(
|
|
"/items/extract-label?mode=item",
|
|
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
|
|
headers={"Authorization": f"Bearer {user_token}"}
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
# Item count should be unchanged — extraction never auto-saves
|
|
final_count = test_db.query(Item).count()
|
|
assert initial_count == final_count
|