Files
MoviePilot/tests/test_message_posters.py

123 lines
5.6 KiB
Python

"""
验证通知独立海报经过渲染、持久化、查询和事件投影,且不改变渠道图片。
"""
import asyncio
import importlib
from uuid import uuid4
import pytest
import sqlalchemy as sa
from alembic.migration import MigrationContext
from alembic.operations import Operations
from app.api.endpoints.message import get_notification_message
from app.application.messaging.message import MessageQueryService, MessageTemplateHelper
from app.chain.base import ChainBase
from app.db.oper.message import MessageOper
from app.db.session import AsyncSessionFactory
from app.domain.context import MediaInfo, MusicInfo
from app.schemas.message import Message
@pytest.mark.parametrize("has_poster", [True, False])
def test_render_preserves_backdrop_and_projects_only_real_poster(has_poster: bool) -> None:
"""
即使消息已经有正文也应提取独立海报,缺失时不得从背景图回退。
"""
media = MediaInfo(
title="测试影片",
backdrop_path="https://images.example.com/backdrop.jpg",
poster_path="https://images.example.com/poster.jpg" if has_poster else None,
)
message = Message(title="已完成整理", image=media.get_message_image())
rendered = MessageTemplateHelper.render(message, mediainfo=media)
assert rendered is message
assert message.image == "https://images.example.com/backdrop.jpg"
assert message.poster == ("https://images.example.com/poster.jpg" if has_poster else None)
event = ChainBase._build_notice_message_data(message)
assert event["poster"] == message.poster
assert event["image"] == message.image
def test_render_keeps_explicit_poster_and_supports_music_cover() -> None:
"""
显式海报优先,音乐媒体使用已有封面,无媒体上下文不猜测图片类型。
"""
music = MusicInfo(title="测试专辑", cover_url="https://images.example.com/album.jpg")
message = Message(title="入库完成", poster="https://images.example.com/custom.jpg")
assert MessageTemplateHelper.render(message, mediainfo=music).poster == message.poster
assert MessageTemplateHelper.render(Message(title="专辑通知"), mediainfo=music).poster == music.cover_url
assert MessageTemplateHelper.render(Message(title="普通通知", image=music.cover_url)).poster is None
@pytest.mark.parametrize("async_write", [False, True])
def test_notification_history_preserves_poster_and_legacy_null(async_write: bool) -> None:
"""
同步和异步写入均应通过真实通知查询返回独立海报,同时兼容旧消息。
"""
title = f"poster-contract-{uuid4().hex}"
payload = {
"title": title,
"image": "https://images.example.com/backdrop.jpg",
"poster": "https://images.example.com/poster.jpg",
}
async def write_and_query() -> None:
"""
仅使用测试临时数据库保存消息,并经 API schema 读取结果。
"""
async with AsyncSessionFactory() as db:
if async_write:
await MessageOper(db).async_add(**payload)
await MessageOper(db).async_add(title=f"{title}-legacy", image=payload["image"])
await db.commit()
else:
MessageOper().add(**payload)
MessageOper().add(title=f"{title}-legacy", image=payload["image"])
messages = await get_notification_message(service=MessageQueryService(MessageOper(db)))
current = next(item for item in messages if item.title == title)
legacy = next(item for item in messages if item.title == f"{title}-legacy")
assert current.image == payload["image"]
assert current.poster == payload["poster"]
assert legacy.image == payload["image"]
assert legacy.poster is None
asyncio.run(write_and_query())
@pytest.mark.parametrize("already_created", [False, True])
def test_message_poster_migration_preserves_existing_images(monkeypatch, already_created: bool) -> None:
"""
迁移支持旧表和新 ORM 已建表,重复执行及回滚不会改写历史背景图。
"""
migration = importlib.import_module("database.versions.e4a8c7b29016_3_1_9")
engine = sa.create_engine("sqlite://")
metadata = sa.MetaData()
columns = [sa.Column("id", sa.Integer(), primary_key=True), sa.Column("image", sa.String())]
if already_created:
columns.append(sa.Column("poster", sa.String(), nullable=True))
messages = sa.Table("message", metadata, *columns)
with engine.begin() as connection:
metadata.create_all(connection)
connection.execute(messages.insert(), {"id": 1, "image": "legacy-backdrop.jpg"})
monkeypatch.setattr(migration, "op", Operations(MigrationContext.configure(connection)))
migration.upgrade()
migration.upgrade()
migrated = sa.Table("message", sa.MetaData(), autoload_with=connection)
row = connection.execute(sa.select(migrated)).mappings().one()
assert row["image"] == "legacy-backdrop.jpg"
assert row["poster"] is None
connection.execute(migrated.insert(), {"id": 2, "image": "backdrop.jpg", "poster": "poster.jpg"})
assert connection.execute(sa.select(migrated.c.poster).where(migrated.c.id == 2)).scalar_one() == "poster.jpg"
migration.downgrade()
migration.downgrade()
restored = sa.Table("message", sa.MetaData(), autoload_with=connection)
assert "poster" not in restored.c
assert connection.execute(sa.select(restored.c.image).order_by(restored.c.id)).scalars().all() == [
"legacy-backdrop.jpg", "backdrop.jpg",
]
engine.dispose()