"""Nullable/deprecated worker images retain their documented gateway keys.""" import asyncio import json from types import SimpleNamespace from unittest.mock import AsyncMock import httpx import pytest from starlette.requests import Request from gateway.forward import rewrite_base64_to_url def test_nullable_image_fields_keep_external_names(tmp_path): value = {"hairline_images": [{"image_high_base64": None, "image_low_base64": None, "grown_image_base64": None}], "high_hairline_center_point": None, "face_measure": None} actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True) assert actual == {"hairline_images": [{"image_high_url": None, "image_low_url": None, "grown_image_url": None}], "high_hairline_center_point": None, "face_measure": None} assert not list(tmp_path.iterdir()) def test_nonempty_images_still_rewrite_alongside_null_fields(tmp_path): import base64 import io from PIL import Image buf = io.BytesIO() Image.new("RGBA", (2, 2), (255, 255, 255, 0)).save(buf, format="PNG") value = {"image_middle_base64": base64.b64encode(buf.getvalue()).decode(), "image_high_base64": None} actual = rewrite_base64_to_url(value, "https://example.test", str(tmp_path), rewrite_null_images=True) assert actual["image_middle_url"].startswith("https://example.test/static/annotations/") assert actual["image_high_url"] is None assert len(list(tmp_path.iterdir())) == 1 def test_legacy_nullable_image_field_names_remain_unchanged(tmp_path): value = {"hairline_images": [{"grown_image_base64": None}], "face_measure": None} assert rewrite_base64_to_url(value, "https://example.test", str(tmp_path)) == value assert not list(tmp_path.iterdir()) @pytest.mark.parametrize("path,expected_key", [ ("/api/v1/hairline/generate", "grown_image_base64"), ("/api/v1/hairline/generate-52", "grown_image_url"), ]) def test_proxy_rewrites_null_images_only_for_interface52(tmp_path, monkeypatch, path, expected_key): import gateway.config as config import gateway.forward as forward monkeypatch.setattr(config, "get_config", lambda: { "dispatch": {"request_timeout_seconds": 10}, "shared_password": "test-token", "public_base_url": "https://example.test", "static_dir": str(tmp_path), }) monkeypatch.setattr(forward, "acquire_worker", AsyncMock(return_value=SimpleNamespace(url="http://worker"))) monkeypatch.setattr(forward, "release_worker", AsyncMock()) worker_response = httpx.Response(200, json={"code": 0, "data": {"hairline_images": [ {"grown_image_base64": None}]}}) monkeypatch.setattr(forward, "get_client", lambda: SimpleNamespace(request=AsyncMock(return_value=worker_response))) async def receive(): return {"type": "http.request", "body": b"", "more_body": False} request = Request({"type": "http", "method": "POST", "path": path, "headers": []}, receive) response = asyncio.run(forward.proxy_request(request, path)) assert response.status_code == 200 assert json.loads(response.body)["data"]["hairline_images"] == [{expected_key: None}]