Spaces:
Sleeping
Sleeping
SuZeAI commited on
Commit ·
3135f39
1
Parent(s): 3073d0f
Update try-on tests for the API-based service
Browse filesMock tryon_service.generate instead of CatVTON/AutoMasker; add tests for the
Gemini provider image parsing and the missing-key error path.
- tests/test_tryon.py +74 -77
tests/test_tryon.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
|
|
| 1 |
import pytest
|
| 2 |
-
import torch
|
| 3 |
from fastapi.testclient import TestClient
|
| 4 |
from unittest.mock import patch, MagicMock
|
| 5 |
from app.main import app
|
|
@@ -8,10 +8,17 @@ from app.models.tryon import TryOnTask
|
|
| 8 |
from app.models.user import User
|
| 9 |
from app.core.security import get_password_hash
|
| 10 |
from app.core.database import get_db
|
| 11 |
-
from app.workers.tasks import
|
| 12 |
|
| 13 |
client = TestClient(app)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
@pytest.fixture(autouse=True)
|
| 16 |
def override_db(db_session):
|
| 17 |
# Override get_db dependency in FastAPI app
|
|
@@ -19,6 +26,7 @@ def override_db(db_session):
|
|
| 19 |
yield
|
| 20 |
app.dependency_overrides.clear()
|
| 21 |
|
|
|
|
| 22 |
@pytest.fixture
|
| 23 |
def test_user_and_token(db_session):
|
| 24 |
# Create a user
|
|
@@ -46,11 +54,12 @@ def test_user_and_token(db_session):
|
|
| 46 |
# Clean up overrides
|
| 47 |
app.dependency_overrides.clear()
|
| 48 |
|
|
|
|
| 49 |
def test_trigger_try_on_success(db_session, test_user_and_token):
|
| 50 |
user, item = test_user_and_token
|
| 51 |
|
| 52 |
# We mock the celery task delay
|
| 53 |
-
with patch("app.api.v1.tryon.
|
| 54 |
response = client.post(
|
| 55 |
"/api/v1/try-on/run",
|
| 56 |
json={"garment_item_id": str(item.id)}
|
|
@@ -61,9 +70,10 @@ def test_trigger_try_on_success(db_session, test_user_and_token):
|
|
| 61 |
assert data["garment_item_id"] == str(item.id)
|
| 62 |
mock_delay.assert_called_once_with(data["id"])
|
| 63 |
|
|
|
|
| 64 |
def test_get_try_on_task(db_session, test_user_and_token):
|
| 65 |
user, item = test_user_and_token
|
| 66 |
-
|
| 67 |
# Pre-insert a task
|
| 68 |
task = TryOnTask(
|
| 69 |
user_id=user.id,
|
|
@@ -79,10 +89,11 @@ def test_get_try_on_task(db_session, test_user_and_token):
|
|
| 79 |
data = response.json()
|
| 80 |
assert data["status"] == "PROCESSING"
|
| 81 |
|
|
|
|
| 82 |
def test_celery_task_execution(db_session, test_user_and_token):
|
|
|
|
| 83 |
user, item = test_user_and_token
|
| 84 |
|
| 85 |
-
# Pre-insert a task
|
| 86 |
task = TryOnTask(
|
| 87 |
user_id=user.id,
|
| 88 |
garment_item_id=item.id,
|
|
@@ -97,88 +108,74 @@ def test_celery_task_execution(db_session, test_user_and_token):
|
|
| 97 |
class SafeSessionWrapper:
|
| 98 |
def __init__(self, session):
|
| 99 |
self._session = session
|
|
|
|
| 100 |
def __getattr__(self, name):
|
| 101 |
if name == "close":
|
| 102 |
return lambda: None
|
| 103 |
return getattr(self._session, name)
|
| 104 |
|
| 105 |
-
#
|
| 106 |
-
def
|
| 107 |
mock_resp = MagicMock()
|
| 108 |
mock_resp.status_code = 200
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
"choices": [
|
| 112 |
-
{
|
| 113 |
-
"message": {
|
| 114 |
-
"content": "A photo of a fair-skinned woman standing with brown hair"
|
| 115 |
-
}
|
| 116 |
-
}
|
| 117 |
-
]
|
| 118 |
-
}
|
| 119 |
-
elif "generativelanguage.googleapis.com" in url:
|
| 120 |
-
import base64
|
| 121 |
-
# 1x1 pixel base64 image
|
| 122 |
-
dummy_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
| 123 |
-
mock_resp.json.return_value = {
|
| 124 |
-
"predictions": [
|
| 125 |
-
{
|
| 126 |
-
"bytesBase64Encoded": dummy_b64
|
| 127 |
-
}
|
| 128 |
-
]
|
| 129 |
-
}
|
| 130 |
-
elif "api.openai.com" in url:
|
| 131 |
-
dummy_b64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
| 132 |
-
mock_resp.json.return_value = {
|
| 133 |
-
"data": [
|
| 134 |
-
{
|
| 135 |
-
"b64_json": dummy_b64
|
| 136 |
-
}
|
| 137 |
-
]
|
| 138 |
-
}
|
| 139 |
return mock_resp
|
| 140 |
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
mock_resp = MagicMock()
|
| 159 |
mock_resp.status_code = 200
|
| 160 |
-
mock_resp.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
return mock_resp
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
assert updated_task.status == "SUCCESS"
|
| 183 |
-
assert updated_task.result_url == "http://localhost:8002/static/tryon_result.jpg"
|
| 184 |
-
mock_upload.assert_called_once()
|
|
|
|
| 1 |
+
import base64
|
| 2 |
import pytest
|
|
|
|
| 3 |
from fastapi.testclient import TestClient
|
| 4 |
from unittest.mock import patch, MagicMock
|
| 5 |
from app.main import app
|
|
|
|
| 8 |
from app.models.user import User
|
| 9 |
from app.core.security import get_password_hash
|
| 10 |
from app.core.database import get_db
|
| 11 |
+
from app.workers.tasks import run_tryon
|
| 12 |
|
| 13 |
client = TestClient(app)
|
| 14 |
|
| 15 |
+
# A valid 1x1 PNG, base64-encoded — small enough to pass through PIL in tests.
|
| 16 |
+
DUMMY_PNG_B64 = (
|
| 17 |
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
|
| 18 |
+
)
|
| 19 |
+
DUMMY_PNG_BYTES = base64.b64decode(DUMMY_PNG_B64)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
@pytest.fixture(autouse=True)
|
| 23 |
def override_db(db_session):
|
| 24 |
# Override get_db dependency in FastAPI app
|
|
|
|
| 26 |
yield
|
| 27 |
app.dependency_overrides.clear()
|
| 28 |
|
| 29 |
+
|
| 30 |
@pytest.fixture
|
| 31 |
def test_user_and_token(db_session):
|
| 32 |
# Create a user
|
|
|
|
| 54 |
# Clean up overrides
|
| 55 |
app.dependency_overrides.clear()
|
| 56 |
|
| 57 |
+
|
| 58 |
def test_trigger_try_on_success(db_session, test_user_and_token):
|
| 59 |
user, item = test_user_and_token
|
| 60 |
|
| 61 |
# We mock the celery task delay
|
| 62 |
+
with patch("app.api.v1.tryon.run_tryon.delay") as mock_delay:
|
| 63 |
response = client.post(
|
| 64 |
"/api/v1/try-on/run",
|
| 65 |
json={"garment_item_id": str(item.id)}
|
|
|
|
| 70 |
assert data["garment_item_id"] == str(item.id)
|
| 71 |
mock_delay.assert_called_once_with(data["id"])
|
| 72 |
|
| 73 |
+
|
| 74 |
def test_get_try_on_task(db_session, test_user_and_token):
|
| 75 |
user, item = test_user_and_token
|
| 76 |
+
|
| 77 |
# Pre-insert a task
|
| 78 |
task = TryOnTask(
|
| 79 |
user_id=user.id,
|
|
|
|
| 89 |
data = response.json()
|
| 90 |
assert data["status"] == "PROCESSING"
|
| 91 |
|
| 92 |
+
|
| 93 |
def test_celery_task_execution(db_session, test_user_and_token):
|
| 94 |
+
"""The try-on task should load the garment, call the image-gen service and upload."""
|
| 95 |
user, item = test_user_and_token
|
| 96 |
|
|
|
|
| 97 |
task = TryOnTask(
|
| 98 |
user_id=user.id,
|
| 99 |
garment_item_id=item.id,
|
|
|
|
| 108 |
class SafeSessionWrapper:
|
| 109 |
def __init__(self, session):
|
| 110 |
self._session = session
|
| 111 |
+
|
| 112 |
def __getattr__(self, name):
|
| 113 |
if name == "close":
|
| 114 |
return lambda: None
|
| 115 |
return getattr(self._session, name)
|
| 116 |
|
| 117 |
+
# Garment image is fetched over HTTP (no local /static file in tests).
|
| 118 |
+
def mock_get_responses(url, *args, **kwargs):
|
| 119 |
mock_resp = MagicMock()
|
| 120 |
mock_resp.status_code = 200
|
| 121 |
+
mock_resp.content = DUMMY_PNG_BYTES
|
| 122 |
+
mock_resp.raise_for_status = lambda: None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return mock_resp
|
| 124 |
|
| 125 |
+
with patch("app.workers.tasks.SessionLocal", return_value=SafeSessionWrapper(db_session)):
|
| 126 |
+
# Image generation is provider-specific; mock the service so the task is provider-agnostic.
|
| 127 |
+
with patch("app.workers.tasks.tryon_service.generate", return_value=DUMMY_PNG_BYTES) as mock_generate:
|
| 128 |
+
with patch(
|
| 129 |
+
"app.workers.tasks.storage_service.upload_file",
|
| 130 |
+
return_value="http://localhost:8002/static/tryon_result.jpg",
|
| 131 |
+
) as mock_upload:
|
| 132 |
+
with patch("app.workers.tasks.requests.get", side_effect=mock_get_responses):
|
| 133 |
+
res = run_tryon(str(task_id_val))
|
| 134 |
+
|
| 135 |
+
assert res is True
|
| 136 |
+
mock_generate.assert_called_once()
|
| 137 |
+
# garments are passed as (bytes, category) tuples
|
| 138 |
+
_, kwargs = mock_generate.call_args
|
| 139 |
+
assert kwargs["garments"][0][1] == "shirt"
|
| 140 |
+
|
| 141 |
+
updated_task = db_session.query(TryOnTask).filter(TryOnTask.id == task_id_val).first()
|
| 142 |
+
assert updated_task.status == "SUCCESS"
|
| 143 |
+
assert updated_task.result_url == "http://localhost:8002/static/tryon_result.jpg"
|
| 144 |
+
mock_upload.assert_called_once()
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def test_tryon_service_gemini_parses_image():
|
| 148 |
+
"""The Gemini provider should decode the inlineData image from a generateContent response."""
|
| 149 |
+
from app.services import tryon_service as ts_module
|
| 150 |
+
|
| 151 |
+
def mock_post(url, *args, **kwargs):
|
| 152 |
+
assert "generativelanguage.googleapis.com" in url
|
| 153 |
mock_resp = MagicMock()
|
| 154 |
mock_resp.status_code = 200
|
| 155 |
+
mock_resp.raise_for_status = lambda: None
|
| 156 |
+
mock_resp.json.return_value = {
|
| 157 |
+
"candidates": [
|
| 158 |
+
{"content": {"parts": [{"inlineData": {"mimeType": "image/png", "data": DUMMY_PNG_B64}}]}}
|
| 159 |
+
]
|
| 160 |
+
}
|
| 161 |
return mock_resp
|
| 162 |
|
| 163 |
+
with patch.object(ts_module.settings, "TRYON_PROVIDER", "gemini"), \
|
| 164 |
+
patch.object(ts_module.settings, "GEMINI_API_KEY", "test-gemini-key"), \
|
| 165 |
+
patch.object(ts_module.settings, "GEMINI_IMAGE_MODEL", "gemini-2.5-flash-image"), \
|
| 166 |
+
patch.object(ts_module.requests, "post", side_effect=mock_post):
|
| 167 |
+
svc = ts_module.TryOnService()
|
| 168 |
+
out = svc.generate(person_bytes=DUMMY_PNG_BYTES, garments=[(DUMMY_PNG_BYTES, "shirt")])
|
| 169 |
+
assert out == DUMMY_PNG_BYTES
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_tryon_service_disabled_raises():
|
| 173 |
+
"""With no API key configured, generate() should raise a clear error (task -> FAILED)."""
|
| 174 |
+
from app.services import tryon_service as ts_module
|
| 175 |
+
|
| 176 |
+
with patch.object(ts_module.settings, "TRYON_PROVIDER", "gemini"), \
|
| 177 |
+
patch.object(ts_module.settings, "GEMINI_API_KEY", None):
|
| 178 |
+
svc = ts_module.TryOnService()
|
| 179 |
+
assert svc.enabled is False
|
| 180 |
+
with pytest.raises(RuntimeError):
|
| 181 |
+
svc.generate(person_bytes=DUMMY_PNG_BYTES, garments=[(DUMMY_PNG_BYTES, "shirt")])
|
|
|
|
|
|
|
|
|