Files
life-echo/api/tests/test_chapters_router_images.py

190 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import unittest
from unittest.mock import Mock, patch
from app.features.memoir.helpers import chapter_to_dict as _chapter_to_dict
from app.features.memoir.memoir_images.storage import CosDownloadUrlError
def _image_stub(**kwargs):
"""构造具有 .placeholder 等属性的 image 对象,供 memoir_image_to_dict 使用。"""
defaults = {
"placeholder": "",
"description": "",
"order_index": 0,
"status": "pending",
"prompt": None,
"url": None,
"storage_key": None,
"provider": None,
"style": None,
"size": None,
"error": None,
"retryable": None,
"created_at": None,
"updated_at": None,
"section_id": None,
}
defaults.update(kwargs)
return type("ImageStub", (), defaults)()
def _chapter_stub(*, images=None, sections=None):
"""构造带 images/sections 的 chapter stub。images 为 image_stub 列表section_id=None 的作封面sections 为 section 列表(含 image_record"""
images = images or []
sections = sections or []
return type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"content": "",
"order_index": 0,
"status": "completed",
"category": "childhood",
"images": images,
"sections": sections,
"cover_image": None,
"updated_at": None,
"is_new": False,
"source_segments": [],
},
)()
class ChaptersRouterImagesTest(unittest.TestCase):
@patch("app.features.memoir.router.TencentCosStorageService")
@patch.dict(
os.environ,
{
"TENCENT_COS_BUCKET": "life-echo-dev-1319381411",
"TENCENT_COS_REGION": "ap-shanghai",
"TENCENT_COS_BASE_URL": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com",
},
clear=False,
)
def test_chapter_to_dict_returns_signed_image_urls_for_response(self, storage_cls):
storage = Mock()
storage.get_download_url.return_value = "https://signed.example.com/memoirs/u1/c1/0-demo.png?sig=123"
storage_cls.from_settings.return_value = storage
img0 = _image_stub(
placeholder="{{IMAGE:南方小镇的青石板路}}",
description="南方小镇的青石板路",
status="completed",
prompt="A serene southern China town",
url="https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
storage_key="memoirs/u1/c1/0-demo.png",
section_id=None,
order_index=0,
)
sec = type("SectionStub", (), {"content": "", "order_index": 0, "image_record": img0, "image_id": None})()
chapter = _chapter_stub(images=[img0], sections=[sec])
payload = _chapter_to_dict(chapter)
self.assertEqual(
payload["images"][0]["url"],
"https://signed.example.com/memoirs/u1/c1/0-demo.png?sig=123",
)
self.assertEqual(payload["images"][0]["prompt"], "A serene southern China town")
self.assertNotIn("storage_key", payload["images"][0])
@patch("app.features.memoir.router.TencentCosStorageService")
@patch.dict(
os.environ,
{
"TENCENT_COS_BUCKET": "life-echo-dev-1319381411",
"TENCENT_COS_REGION": "ap-shanghai",
"TENCENT_COS_BASE_URL": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com",
},
clear=False,
)
def test_chapter_to_dict_preserves_completed_asset_when_signing_fails(self, storage_cls):
storage = Mock()
storage.get_download_url.side_effect = CosDownloadUrlError(
"cos unavailable", retryable=True, request_id="req-err"
)
storage_cls.from_settings.return_value = storage
img0 = _image_stub(
placeholder="{{IMAGE:南方小镇的青石板路}}",
description="南方小镇的青石板路",
status="completed",
prompt="A serene southern China town",
url="https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0-demo.png",
storage_key="memoirs/u1/c1/0-demo.png",
section_id=None,
order_index=0,
)
sec = type("SectionStub", (), {"content": "", "order_index": 0, "image_record": img0, "image_id": None})()
chapter = _chapter_stub(images=[img0], sections=[sec])
payload = _chapter_to_dict(chapter)
self.assertEqual(payload["images"][0]["status"], "completed")
self.assertIsNone(payload["images"][0]["url"])
self.assertEqual(payload["images"][0]["prompt"], "A serene southern China town")
self.assertEqual(payload["images"][0]["error"], "image delivery unavailable")
self.assertNotIn("storage_key", payload["images"][0])
@patch("app.features.memoir.router.TencentCosStorageService")
def test_chapter_to_dict_drops_malformed_image_assets(self, storage_cls):
storage_cls.from_settings.return_value = Mock()
# 无 sections 时 content/images 来自 _sections_to_content_and_images 得到 []无有效封面images 的 section_id 非空)
img = _image_stub(status="completed", placeholder="", description="", section_id="sec1")
chapter = _chapter_stub(images=[img], sections=[])
payload = _chapter_to_dict(chapter)
self.assertEqual(payload["images"], [])
@patch("app.features.memoir.router.MemoirImageSettings")
@patch("app.features.memoir.router.TencentCosStorageService")
def test_chapter_to_dict_hides_non_completed_assets_when_feature_disabled(self, storage_cls, memoir_img_settings_cls):
storage = Mock()
storage.get_download_url.return_value = "https://signed.example.com/0.png?sig=123"
storage_cls.from_settings.return_value = storage
memoir_img_settings_cls.from_settings.return_value = Mock(enabled=False)
# 仅一个 completed 的 sectionenabled=False 时 completed_image_assets 只保留 completed故仍为 1 条
img_completed = _image_stub(
placeholder="{{IMAGE:奶奶坐在院子里的藤椅上}}",
description="奶奶坐在院子里的藤椅上",
status="completed",
url="https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/1-demo.png",
storage_key="memoirs/u1/c1/1-demo.png",
order_index=0,
section_id="s1",
)
sec = type("SectionStub", (), {"content": "", "order_index": 0, "image_record": img_completed, "image_id": None})()
chapter = _chapter_stub(images=[img_completed], sections=[sec])
payload = _chapter_to_dict(chapter)
self.assertEqual(len(payload["images"]), 1)
self.assertEqual(payload["images"][0]["status"], "completed")
@patch("app.features.memoir.router.TencentCosStorageService")
@patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False)
def test_chapter_to_dict_preserves_retryable_flag_for_failed_assets(self, storage_cls):
storage_cls.from_settings.return_value = Mock()
img = _image_stub(
placeholder="{{IMAGE:南方小镇的青石板路}}",
description="南方小镇的青石板路",
status="failed",
url=None,
error="upload denied",
retryable=False,
order_index=0,
)
sec = type("SectionStub", (), {"content": "", "order_index": 0, "image_record": img, "image_id": None})()
chapter = _chapter_stub(images=[img], sections=[sec])
payload = _chapter_to_dict(chapter)
self.assertEqual(payload["images"][0]["status"], "failed")
self.assertFalse(payload["images"][0]["retryable"])