SuZeAI commited on
Commit
3135f39
·
1 Parent(s): 3073d0f

Update try-on tests for the API-based service

Browse files

Mock tryon_service.generate instead of CatVTON/AutoMasker; add tests for the
Gemini provider image parsing and the missing-key error path.

Files changed (1) hide show
  1. 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 run_idm_vton_tryon
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.run_idm_vton_tryon.delay") as mock_delay:
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
- # Mock responses for NVIDIA VLM and Gemini Imagen
106
- def mock_post_responses(url, *args, **kwargs):
107
  mock_resp = MagicMock()
108
  mock_resp.status_code = 200
109
- if "integrate.api.nvidia.com" in url:
110
- mock_resp.json.return_value = {
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
- # Mock AutoMasker
142
- mock_automasker = MagicMock()
143
- from PIL import Image
144
- dummy_mask = Image.new("L", (768, 1024), 255)
145
- mock_automasker.return_value = {"mask": dummy_mask}
146
-
147
- mock_mask_processor = MagicMock()
148
- mock_mask_processor.blur.return_value = dummy_mask
149
-
150
- # Mock CatVTON Pipeline
151
- mock_pipeline = MagicMock()
152
- def mock_pipeline_call(image, condition_image, mask, **kwargs):
153
- return [image]
154
- mock_pipeline.side_effect = mock_pipeline_call
155
-
156
- # Mock requests.get to return a dummy 1x1 PNG image
157
- def mock_get_responses(url, *args, **kwargs):
 
 
 
 
 
 
 
 
 
 
 
158
  mock_resp = MagicMock()
159
  mock_resp.status_code = 200
160
- mock_resp.content = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15c4\x00\x00\x00\rIDATx\x9cc`\x00\x00\x00\x02\x00\x01H\xaf\xa4q\x00\x00\x00\x00IEND\xaeB`\x82'
 
 
 
 
 
161
  return mock_resp
162
 
163
- # Mock SessionLocal in app.workers.tasks to return our wrapped session
164
- with patch("app.workers.tasks.SessionLocal", return_value=SafeSessionWrapper(db_session)):
165
- # Mock get_automasker and get_catvton_pipeline to avoid loading models
166
- with patch("app.workers.tasks.get_automasker", return_value=(mock_automasker, mock_mask_processor)):
167
- with patch("app.workers.tasks.get_catvton_pipeline", return_value=mock_pipeline):
168
- # Mock the storage service upload_file to return a valid URL
169
- with patch("app.workers.tasks.storage_service.upload_file", return_value="http://localhost:8002/static/tryon_result.jpg") as mock_upload:
170
- # Mock requests.post to avoid real network calls
171
- with patch("app.workers.tasks.requests.post", side_effect=mock_post_responses) as mock_post:
172
- # Mock requests.get to return a dummy PNG image
173
- with patch("app.workers.tasks.requests.get", side_effect=mock_get_responses) as mock_get:
174
- # Patch settings to ensure OpenAI API key is set for test
175
- with patch("app.workers.tasks.settings.OPENAI_API_KEY", "test-openai-key"):
176
- res = run_idm_vton_tryon(str(task_id_val))
177
-
178
- assert res is True
179
-
180
- # Reload task from DB
181
- updated_task = db_session.query(TryOnTask).filter(TryOnTask.id == task_id_val).first()
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")])