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

199 lines
7.1 KiB
Python
Raw Normal View History

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,
}
defaults.update(kwargs)
return type("ImageStub", (), defaults)()
2026-03-20 15:15:35 +08:00
def _chapter_stub(*, images=None, canonical_markdown="正文"):
"""stories-first章节配图均为 chapter 级 MemoirImage按 order_index 取封面)。"""
images = images or []
return type(
"ChapterStub",
(),
{
"id": "chapter-1",
"title": "童年的夏天",
"content": "",
"order_index": 0,
"status": "completed",
"category": "childhood",
2026-03-20 15:15:35 +08:00
"canonical_markdown": canonical_markdown,
"images": images,
"cover_image": None,
2026-03-20 15:15:35 +08:00
"cover_asset_id": None,
"updated_at": None,
"is_new": False,
"source_segments": [],
},
)()
class ChaptersRouterImagesTest(unittest.TestCase):
2026-03-20 15:15:35 +08:00
@patch("app.features.memoir.helpers.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()
2026-03-19 14:36:14 +08:00
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",
order_index=0,
)
2026-03-20 15:15:35 +08:00
chapter = _chapter_stub(images=[img0])
payload = _chapter_to_dict(chapter)
2026-03-20 15:15:35 +08:00
self.assertEqual(payload["images"], [])
self.assertEqual(
2026-03-20 15:15:35 +08:00
payload["cover_image"]["url"],
"https://signed.example.com/memoirs/u1/c1/0-demo.png?sig=123",
)
2026-03-20 15:15:35 +08:00
self.assertEqual(
payload["cover_image"]["prompt"], "A serene southern China town"
)
self.assertNotIn("storage_key", payload["cover_image"])
2026-03-20 15:15:35 +08:00
@patch("app.features.memoir.helpers.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,
)
2026-03-19 14:36:14 +08:00
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",
order_index=0,
)
2026-03-20 15:15:35 +08:00
chapter = _chapter_stub(images=[img0])
payload = _chapter_to_dict(chapter)
2026-03-20 15:15:35 +08:00
self.assertEqual(payload["images"], [])
self.assertEqual(payload["cover_image"]["status"], "completed")
self.assertIsNone(payload["cover_image"]["url"])
self.assertEqual(
payload["cover_image"]["prompt"], "A serene southern China town"
)
self.assertEqual(payload["cover_image"]["error"], "image delivery unavailable")
self.assertNotIn("storage_key", payload["cover_image"])
2026-03-20 15:15:35 +08:00
@patch("app.features.memoir.helpers.TencentCosStorageService")
def test_chapter_to_dict_inline_images_list_empty(self, storage_cls):
storage_cls.from_settings.return_value = Mock()
2026-03-19 14:36:14 +08:00
img = _image_stub(
2026-03-20 15:15:35 +08:00
status="completed", placeholder="", description="", order_index=0
2026-03-19 14:36:14 +08:00
)
2026-03-20 15:15:35 +08:00
chapter = _chapter_stub(images=[img])
payload = _chapter_to_dict(chapter)
self.assertEqual(payload["images"], [])
2026-03-20 15:15:35 +08:00
@patch("app.features.memoir.helpers.MemoirImageSettings")
@patch("app.features.memoir.helpers.TencentCosStorageService")
2026-03-19 14:36:14 +08:00
def test_chapter_to_dict_hides_non_completed_assets_when_feature_disabled(
self, storage_cls, memoir_img_settings_cls
):
storage = Mock()
2026-03-19 14:36:14 +08:00
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)
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,
)
2026-03-20 15:15:35 +08:00
chapter = _chapter_stub(images=[img_completed])
payload = _chapter_to_dict(chapter)
2026-03-20 15:15:35 +08:00
self.assertEqual(payload["images"], [])
self.assertEqual(payload["cover_image"]["status"], "completed")
2026-03-20 15:15:35 +08:00
@patch("app.features.memoir.helpers.TencentCosStorageService")
@patch.dict(os.environ, {"MEMOIR_IMAGE_ENABLED": "true"}, clear=False)
2026-03-19 14:36:14 +08:00
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,
)
2026-03-20 15:15:35 +08:00
chapter = _chapter_stub(images=[img])
payload = _chapter_to_dict(chapter)
2026-03-20 15:15:35 +08:00
self.assertEqual(payload["images"], [])
self.assertEqual(payload["cover_image"]["status"], "failed")
self.assertFalse(payload["cover_image"]["retryable"])