study / test /test_image_tasks_api.py
xiaolongmr
添加编辑
638cad0
Raw History Blame Contribute Delete
13 kB
from __future__ import annotations
import base64
import unittest
from unittest import mock
from fastapi import FastAPI
from fastapi.testclient import TestClient
import api.image_tasks as image_tasks_module
from services.third_party_image_service import ThirdPartyRoute
AUTH_HEADERS = {"Authorization": "Bearer chatgpt2api"}
PNG_BYTES = b"\x89PNG\r\n\x1a\n"
DATA_IMAGE_URL = f"data:image/png;base64,{base64.b64encode(PNG_BYTES).decode('ascii')}"
class FakeImageTaskService:
def __init__(self):
self.generation_calls = []
self.edit_calls = []
def submit_generation(self, identity, **kwargs):
self.generation_calls.append((identity, kwargs))
return {
"id": kwargs["client_task_id"],
"status": "success",
"mode": "generate",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00",
"data": [{"url": f"{kwargs['base_url']}/images/fake.png"}],
}
def submit_edit(self, identity, **kwargs):
self.edit_calls.append((identity, kwargs))
return {
"id": kwargs["client_task_id"],
"status": "queued",
"mode": "edit",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00",
}
def list_tasks(self, _identity, ids):
return {
"items": [
{
"id": task_id,
"status": "success",
"mode": "generate",
"created_at": "2026-01-01 00:00:00",
"updated_at": "2026-01-01 00:00:00",
"data": [{"url": "http://testserver/images/fake.png"}],
}
for task_id in ids
if task_id != "missing"
],
"missing_ids": [task_id for task_id in ids if task_id == "missing"],
}
class FakeThirdPartyImageService:
def __init__(self):
self.find_route_calls: list[dict[str, object]] = []
def find_route(self, model, **kwargs):
self.find_route_calls.append(dict(kwargs))
return ThirdPartyRoute(
group={"id": "g1", "billing_mode": "general_multiplier", "default_cost": 1},
model={"id": str(model), "upstream_model": str(model), "resolution": kwargs.get("resolution") or "1k", "default_cost": 1},
)
def assert_access(self, _identity, _route):
return None
def cost_for_route(self, _route, _quality, _image_count):
return 1
class FakeFreeNamedThirdPartyImageService:
def find_named_route(self, model, **_kwargs):
return ThirdPartyRoute(
group={"id": "g-free", "billing_mode": "general_multiplier", "default_cost": 1, "multiplier_enabled": True},
model={"id": str(model), "upstream_model": "gpt-image-2", "resolution": "1k", "default_cost": 0},
)
def find_route(self, model, **kwargs):
return self.find_named_route(model, **kwargs)
def assert_access(self, _identity, _route):
return None
def cost_for_route(self, _route, _quality, _image_count):
return 0
class ImageTasksApiTests(unittest.TestCase):
def setUp(self):
self.fake_service = FakeImageTaskService()
self.identity = {"id": "admin", "role": "admin", "name": "Admin", "account_pool_enabled": True}
self.service_patcher = mock.patch.object(image_tasks_module, "image_task_service", self.fake_service)
self.identity_patcher = mock.patch.object(image_tasks_module, "require_identity", lambda _authorization: self.identity)
self.service_patcher.start()
self.identity_patcher.start()
self.addCleanup(self.service_patcher.stop)
self.addCleanup(self.identity_patcher.stop)
app = FastAPI()
app.include_router(image_tasks_module.create_router())
self.client = TestClient(app)
def test_create_generation_task(self):
response = self.client.post(
"/api/image-tasks/generations",
headers=AUTH_HEADERS,
json={"client_task_id": "task-1", "prompt": "cat", "model": "gpt-image-2"},
)
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertEqual(payload["id"], "task-1")
self.assertEqual(payload["status"], "success")
self.assertEqual(len(self.fake_service.generation_calls), 1)
self.assertEqual(self.fake_service.generation_calls[0][1]["account_pool_strategy"], "own_first")
response = self.client.post(
"/api/image-tasks/generations",
headers=AUTH_HEADERS,
json={
"client_task_id": "task-2",
"prompt": "cat",
"model": "gpt-image-2",
"account_pool_strategy": "shared_first",
},
)
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(self.fake_service.generation_calls[-1][1]["account_pool_strategy"], "shared_first")
def test_disabled_personal_pool_forces_admin_pool(self):
disabled_identity = {
"id": "normal-1",
"role": "normal",
"name": "Normal",
"account_pool_enabled": False,
}
self.identity = disabled_identity
response = self.client.post(
"/api/image-tasks/generations",
headers=AUTH_HEADERS,
json={
"client_task_id": "task-disabled",
"prompt": "cat",
"model": "gpt-image-2",
"account_pool_strategy": "shared_first",
},
)
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(self.fake_service.generation_calls[-1][1]["account_pool_strategy"], "admin_only")
def test_normal_pool_task_receives_submit_time_price_snapshot(self):
self.identity = {
"id": "normal-1",
"role": "normal",
"name": "Normal",
"account_pool_enabled": True,
}
quote = {
"base_unit_price": 1,
"pay_percentage": 50,
"unit_price": 0.5,
"estimated_total": 0.5,
"rule_id": "weekday-offpeak",
"rule_name": "工作日低谷",
"evaluated_at": "2026-09-02T07:00:00+08:00",
}
with mock.patch.object(image_tasks_module.pool_pricing_service, "quote", return_value=quote):
response = self.client.post(
"/api/image-tasks/generations",
headers=AUTH_HEADERS,
json={"client_task_id": "priced-task", "prompt": "cat", "model": "gpt-image-2"},
)
self.assertEqual(response.status_code, 200, response.text)
payload = self.fake_service.generation_calls[-1][1]
self.assertEqual(payload["credit_user_id"], "normal-1")
self.assertEqual(payload["credit_amount"], 0.5)
self.assertEqual(payload["credit_pricing"], quote)
def test_create_edit_task_accepts_multiple_images(self):
"""测试图片编辑任务接口支持多个上传图片。"""
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={"client_task_id": "edit-1", "prompt": "edit", "model": "gpt-image-2"},
files=[
("image", ("one.png", b"one", "image/png")),
("image", ("two.png", b"two", "image/png")),
],
)
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(response.json()["id"], "edit-1")
self.assertEqual(len(self.fake_service.edit_calls), 1)
self.assertEqual(self.fake_service.edit_calls[0][1]["account_pool_strategy"], "own_first")
images = self.fake_service.edit_calls[0][1]["images"]
self.assertEqual(len(images), 2)
def test_create_edit_task_forwards_mask_upload(self):
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={"client_task_id": "edit-mask-1", "prompt": "outpaint", "model": "gpt-image-2"},
files=[
("image", ("canvas.png", PNG_BYTES, "image/png")),
("mask", ("mask.png", PNG_BYTES, "image/png")),
],
)
self.assertEqual(response.status_code, 200, response.text)
payload = self.fake_service.edit_calls[-1][1]
self.assertEqual(payload["masks"], [(PNG_BYTES, "mask.png", "image/png")])
def test_create_edit_task_preserves_memory_fields_from_multipart_form(self):
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={
"client_task_id": "edit-memory-1",
"prompt": "continue this image conversation",
"model": "gpt-image-2",
"memory": "true",
"memory_conversation_id": "local-conversation-1",
"memory_reset": "false",
"account_pool_strategy": "shared_first",
},
files=[("image", ("one.png", b"one", "image/png"))],
)
self.assertEqual(response.status_code, 200, response.text)
payload = self.fake_service.edit_calls[-1][1]
self.assertIs(payload["memory_enabled"], True)
self.assertEqual(payload["memory_conversation_id"], "local-conversation-1")
self.assertIs(payload["memory_reset"], False)
self.assertEqual(payload["account_pool_strategy"], "shared_first")
def test_create_edit_task_accepts_image_url(self):
"""测试图片编辑任务接口支持表单 image_url 引用。"""
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={
"client_task_id": "edit-url-1",
"prompt": "edit",
"model": "gpt-image-2",
"image_url": DATA_IMAGE_URL,
},
)
self.assertEqual(response.status_code, 200, response.text)
self.assertEqual(len(self.fake_service.edit_calls), 1)
images = self.fake_service.edit_calls[0][1]["images"]
self.assertEqual(images, [(PNG_BYTES, "image_url.png", "image/png")])
def test_create_edit_task_can_use_third_party_image_api(self):
fake_third_party = FakeThirdPartyImageService()
with mock.patch.object(image_tasks_module, "third_party_image_service", fake_third_party):
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={
"client_task_id": "edit-api-1",
"prompt": "edit",
"model": "gpt-image-2",
"image_url": DATA_IMAGE_URL,
"quality": "high",
"resolution": "1k",
"third_party_image_group_id": "g1",
},
)
self.assertEqual(response.status_code, 200, response.text)
payload = self.fake_service.edit_calls[-1][1]
self.assertEqual(payload["quality"], "high")
self.assertEqual(payload["resolution"], "1k")
self.assertEqual(payload["third_party_image_group_id"], "g1")
self.assertEqual(fake_third_party.find_route_calls[0]["advance"], False)
def test_named_free_api_model_without_group_routes_to_image_api_without_credit_cost(self):
self.identity = {"id": "normal-1", "role": "normal", "name": "Normal", "account_pool_enabled": False}
with mock.patch.object(image_tasks_module, "third_party_image_service", FakeFreeNamedThirdPartyImageService()):
response = self.client.post(
"/api/image-tasks/edits",
headers=AUTH_HEADERS,
data={
"client_task_id": "edit-free-1",
"prompt": "edit",
"model": "gpt-image-2(free)",
"image_url": DATA_IMAGE_URL,
},
)
self.assertEqual(response.status_code, 200, response.text)
payload = self.fake_service.edit_calls[-1][1]
self.assertEqual(payload["resolution"], "1k")
self.assertEqual(payload["third_party_image_group_id"], "g-free")
self.assertEqual(payload["credit_user_id"], "normal-1")
self.assertEqual(payload["credit_amount"], 0)
def test_list_tasks_reports_missing_ids(self):
response = self.client.get("/api/image-tasks?ids=task-1,missing", headers=AUTH_HEADERS)
self.assertEqual(response.status_code, 200, response.text)
payload = response.json()
self.assertEqual([item["id"] for item in payload["items"]], ["task-1"])
self.assertEqual(payload["missing_ids"], ["missing"])
if __name__ == "__main__":
unittest.main()