esmaill1
feat: implement image processing core, FastAPI backend, and full-stack integration tests
f19ba0f | """ | |
| EL HELAL Studio β API Test Suite | |
| Run with: python test_api.py | |
| """ | |
| import requests | |
| import json | |
| import io | |
| import sys | |
| from pathlib import Path | |
| from PIL import Image | |
| BASE_URL = "http://127.0.0.1:9000" | |
| # ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def status(): | |
| r = requests.get(f"{BASE_URL}/status") | |
| r.raise_for_status() | |
| return r.json() | |
| def wait_for_ai(timeout=60): | |
| """Poll /status until ai_ready is True.""" | |
| import time | |
| for _ in range(timeout): | |
| s = status() | |
| if s.get("ai_ready"): | |
| return True | |
| time.sleep(2) | |
| raise TimeoutError("AI model did not become ready within timeout") | |
| def create_test_image(width=400, height=600, color=(100, 150, 200)) -> io.BytesIO: | |
| """Create a synthetic JPEG for testing upload.""" | |
| img = Image.new("RGB", (width, height), color) | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG", quality=85) | |
| buf.seek(0) | |
| buf.name = "test_portrait.jpg" | |
| return buf | |
| def upload_image(file_obj) -> dict: | |
| r = requests.post(f"{BASE_URL}/upload", files={"file": file_obj}) | |
| r.raise_for_status() | |
| return r.json() | |
| # ββ Test Cases ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def test_status(): | |
| print("\n[TEST] GET /status") | |
| data = status() | |
| assert "ai_ready" in data, f"Expected 'ai_ready' key, got: {data}" | |
| print(f" β ai_ready: {data['ai_ready']}") | |
| def test_settings_get(): | |
| print("\n[TEST] GET /settings") | |
| r = requests.get(f"{BASE_URL}/settings") | |
| r.raise_for_status() | |
| data = r.json() | |
| print(f" β settings: {json.dumps(data, indent=4)}") | |
| def test_settings_update(): | |
| print("\n[TEST] POST /settings") | |
| payload = {"retouch": {"enabled": True, "sensitivity": 2.5, "tone_smoothing": 0.5}} | |
| r = requests.post(f"{BASE_URL}/settings", json=payload) | |
| r.raise_for_status() | |
| data = r.json() | |
| assert data.get("status") == "success" | |
| print(f" β status: {data['status']}") | |
| def test_frames_list(): | |
| print("\n[TEST] GET /frames") | |
| r = requests.get(f"{BASE_URL}/frames") | |
| r.raise_for_status() | |
| data = r.json() | |
| assert "frames" in data | |
| print(f" β frames count: {len(data['frames'])}") | |
| def test_upload(): | |
| print("\n[TEST] POST /upload") | |
| img = create_test_image() | |
| data = upload_image(img) | |
| assert "id" in data | |
| assert "thumb_url" in data | |
| print(f" β file_id: {data['id']}") | |
| print(f" β thumb_url: {data['thumb_url']}") | |
| return data # Return full dict for caller | |
| def test_process_image(file_id: str): | |
| print(f"\n[TEST] POST /process/{file_id}") | |
| payload = { | |
| "name": "Test User", | |
| "id_number": "12345", | |
| "do_restore": False, | |
| "fidelity": 0.5, | |
| "do_rmbg": True, | |
| "do_color": True, | |
| "do_retouch": True, | |
| "do_crop": True, | |
| "add_studio_name": True, | |
| "add_logo": True, | |
| "add_date": True, | |
| } | |
| r = requests.post(f"{BASE_URL}/process/{file_id}", data=payload) | |
| r.raise_for_status() | |
| data = r.json() | |
| assert "result_url" in data | |
| print(f" β result_url: {data['result_url']}") | |
| print(f" β preview_url: {data['preview_url']}") | |
| def test_process_image_with_restore(file_id: str): | |
| print(f"\n[TEST] POST /process/{file_id} (with restoration)") | |
| payload = { | |
| "name": "Restored User", | |
| "id_number": "99999", | |
| "do_restore": True, | |
| "fidelity": 0.6, | |
| "do_rmbg": True, | |
| "do_color": True, | |
| "do_retouch": False, | |
| "do_crop": True, | |
| "add_studio_name": True, | |
| "add_logo": False, | |
| "add_date": False, | |
| } | |
| r = requests.post(f"{BASE_URL}/process/{file_id}", data=payload) | |
| r.raise_for_status() | |
| data = r.json() | |
| print(f" β result_url: {data['result_url']}") | |
| def test_process_manual_crop(file_id: str): | |
| print(f"\n[TEST] POST /process/{file_id} (manual crop)") | |
| payload = { | |
| "name": "Manual Crop", | |
| "id_number": "55555", | |
| "x1": 100, | |
| "y1": 50, | |
| "x2": 300, | |
| "y2": 450, | |
| "do_crop": True, | |
| "do_rmbg": True, | |
| "do_color": False, | |
| "do_retouch": False, | |
| "add_studio_name": False, | |
| "add_logo": False, | |
| "add_date": False, | |
| } | |
| r = requests.post(f"{BASE_URL}/process/{file_id}", data=payload) | |
| r.raise_for_status() | |
| data = r.json() | |
| print(f" β result_url: {data['result_url']}") | |
| def test_frames_upload(): | |
| print("\n[TEST] POST /frames (upload a test frame)") | |
| img = Image.new("RGBA", (200, 200), (200, 100, 100, 255)) | |
| buf = io.BytesIO() | |
| img.save(buf, format="PNG") | |
| buf.seek(0) | |
| r = requests.post( | |
| f"{BASE_URL}/frames", | |
| files={"file": ("test_frame.png", buf, "image/png")}, | |
| ) | |
| r.raise_for_status() | |
| data = r.json() | |
| assert data.get("status") == "success" | |
| frame = data["frame"] | |
| print(f" β uploaded: {frame['filename']}") | |
| return frame["filename"] | |
| def test_frames_delete(filename: str): | |
| print(f"\n[TEST] DELETE /frames/{filename}") | |
| r = requests.delete(f"{BASE_URL}/frames/{filename}") | |
| r.raise_for_status() | |
| data = r.json() | |
| assert data.get("status") == "success" | |
| print(f" β deleted: {filename}") | |
| def test_backup_export(): | |
| print("\n[TEST] POST /backup/export") | |
| client_data = {"theme": "dark", "last_user": "tester"} | |
| r = requests.post(f"{BASE_URL}/backup/export", json={"client_data": client_data}) | |
| r.raise_for_status() | |
| assert r.headers["content-type"].startswith("application/zip") | |
| print(f" β Content-Length: {len(r.content)} bytes") | |
| return r.content | |
| def test_backup_import(zip_bytes: bytes): | |
| print("\n[TEST] POST /backup/import") | |
| files = {"file": ("backup.zip", io.BytesIO(zip_bytes), "application/zip")} | |
| r = requests.post(f"{BASE_URL}/backup/import", files=files) | |
| r.raise_for_status() | |
| data = r.json() | |
| print(f" β status: {data['status']}") | |
| def test_clear_all(): | |
| print("\n[TEST] POST /clear-all") | |
| r = requests.post(f"{BASE_URL}/clear-all") | |
| r.raise_for_status() | |
| data = r.json() | |
| assert data.get("status") == "success" | |
| print(f" β removed_count: {data.get('removed_count')}") | |
| # ββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| print("=" * 60) | |
| print("EL HELAL Studio β API Test Suite") | |
| print("=" * 60) | |
| # 1. Status check | |
| test_status() | |
| # 2. Wait for AI to be ready | |
| print("\n[INFO] Waiting for AI models to load...") | |
| try: | |
| wait_for_ai(timeout=90) | |
| print("[INFO] AI models are ready.") | |
| except TimeoutError as e: | |
| print(f"[WARN] {e}") | |
| # 3. Settings | |
| test_settings_get() | |
| test_settings_update() | |
| # 4. Frames | |
| test_frames_list() | |
| # 5. Upload + Process pipeline | |
| upload_data = test_upload() | |
| file_id = upload_data["id"] | |
| test_process_image(file_id) | |
| test_process_image_with_restore(file_id) | |
| test_process_image_with_restore(file_id) # extra run for variance | |
| # 6. Manual crop | |
| upload2 = test_upload() | |
| test_process_manual_crop(upload2["id"]) | |
| # 7. Frames upload/delete cycle | |
| frame_filename = test_frames_upload() | |
| test_frames_delete(frame_filename) | |
| # 8. Backup cycle | |
| zip_bytes = test_backup_export() | |
| test_backup_import(zip_bytes) | |
| # 9. Clear all | |
| test_clear_all() | |
| print("\n" + "=" * 60) | |
| print("ALL TESTS PASSED") | |
| print("=" * 60) | |
| if __name__ == "__main__": | |
| main() | |