#!/usr/bin/env python # -*- coding:utf-8 -*- """ 通知模板渲染回归测试。 V3 起通知模板不再硬编码在程序中,默认模板由数据库升级迁移写入 ``SystemConfigKey.NotificationTemplates``。本测试验证: 1. V3 迁移会把默认模板一次性写入数据库配置,并覆盖用户旧设置; 2. 带双引号条件的 Jinja 模板(如 ``{% if type == "音乐" %}``)可正常渲染, 避免 JSON 序列化转义引号导致音乐下载/入库通知内容为空的历史问题; 3. 模板完全由数据库配置驱动,配置缺失时不渲染、消息保持原样。 """ from pathlib import Path import importlib.util import pytest from app.core.context import MUSIC_ENTITY_ALBUM, MusicInfo from app.core.meta import MetaMusic from app.db.systemconfig_oper import SystemConfigOper from app.helper.message import MessageTemplateHelper, TemplateContextBuilder, TemplateHelper from app.schemas.message import Notification from app.schemas.types import ContentType, SystemConfigKey MUSIC_ORGANIZE_TEMPLATE = """ { 'title': '{{ title_year }}{% if track_number %} #{{ track_number }}{% endif %}' '{% if season_episode %} {{ season_episode }}{% endif %} 已入库', 'text': '类型:{{ type }}{% if category %},类别:{{ category }}{% endif %}' '{% if type == "音乐" and artist %}\\n艺术家:{{ artist }}{% endif %}' '{% if type == "音乐" and album %}\\n专辑:{{ album }}{% endif %}' '{% if type == "音乐" and audio_specs %}\\n音质:{{ audio_specs }}{% endif %}' '{% if resource_term %},质量:{{ resource_term }}{% endif %}' ',共{{ file_count }}个文件,大小:{{ total_size }}' }""" MUSIC_CONTEXT = { "type": "音乐", "title_year": "晴天 (2003)", "track_number": 3, "artist": "周杰伦", "album": "叶惠美", "audio_specs": "FLAC · 24-bit · 96 kHz", "resource_term": "无损", "file_count": 12, "total_size": "1.2 GB", } @pytest.fixture() def notification_templates() -> SystemConfigOper: """备份并还原通知模板配置,避免测试污染其它用例。""" config_oper = SystemConfigOper() original = config_oper.get(SystemConfigKey.NotificationTemplates) yield config_oper config_oper.set(SystemConfigKey.NotificationTemplates, original) def test_album_batch_context_uses_album_title_for_notification() -> None: """ 专辑实体批量入库的通知上下文应以专辑名作为标题, 不得展示批次中某个单曲的曲名和曲序。 """ meta = MetaMusic( title="晴天", artists=["周杰伦"], album="叶惠美", track_number=3, year=2003, ) mediainfo = MusicInfo( music_type=MUSIC_ENTITY_ALBUM, title="晴天", album="叶惠美", artists=["周杰伦"], year=2003, ) context = TemplateContextBuilder().build(meta=meta, mediainfo=mediainfo) assert context["title"] == "叶惠美" assert context["title_year"] == "叶惠美 (2003)" assert context.get("track_number") is None rendered = TemplateHelper().render( template_content=MUSIC_ORGANIZE_TEMPLATE, **dict(context, file_count=11, total_size="1.2 GB"), ) assert isinstance(rendered, dict) assert rendered["title"] == "叶惠美 (2003) 已入库" assert "艺术家:周杰伦" in rendered["text"] assert "#3" not in rendered["title"] def test_single_track_context_keeps_track_title_and_number() -> None: """ 单曲下载/整理的通知上下文应继续使用曲目标题和曲序, 不受专辑场景调整的影响。 """ context = MUSIC_CONTEXT.copy() rendered = TemplateHelper().render( template_content=MUSIC_ORGANIZE_TEMPLATE, **context ) assert isinstance(rendered, dict) assert rendered["title"] == "晴天 (2003) #3 已入库" def test_literal_template_with_quoted_condition_renders_music() -> None: """ 带 ``{% if type == "音乐" %}`` 双引号条件的模板应能正常渲染, 音乐字段(艺术家/专辑/音质)必须出现在渲染结果中。 """ rendered = TemplateHelper().render( template_content=MUSIC_ORGANIZE_TEMPLATE, **MUSIC_CONTEXT ) assert isinstance(rendered, dict) assert rendered["title"] == "晴天 (2003) #3 已入库" assert "艺术家:周杰伦" in rendered["text"] assert "专辑:叶惠美" in rendered["text"] assert "音质:FLAC · 24-bit · 96 kHz" in rendered["text"] def test_literal_template_skips_music_fields_for_video() -> None: """ 影视场景下模板中的音乐专属字段不应出现,影视字段正常渲染。 """ rendered = TemplateHelper().render( template_content=MUSIC_ORGANIZE_TEMPLATE, type="电视剧", title_year="测试剧集 (2025)", season_episode="S01E05", category="国产剧", resource_term="1080p", file_count=1, total_size="5 GB", ) assert isinstance(rendered, dict) assert rendered["title"] == "测试剧集 (2025) S01E05 已入库" assert "艺术家" not in rendered["text"] assert "专辑" not in rendered["text"] assert "类别:国产剧" in rendered["text"] def test_message_renders_from_db_config(notification_templates: SystemConfigOper) -> None: """ MessageTemplateHelper 应使用数据库配置的模板渲染消息, 而不再依赖程序内硬编码模板。 """ notification_templates.set( SystemConfigKey.NotificationTemplates, {"organizeSuccess": MUSIC_ORGANIZE_TEMPLATE}, ) message = Notification(ctype=ContentType.OrganizeSuccess) MessageTemplateHelper.render(message, **MUSIC_CONTEXT) assert message.title == "晴天 (2003) #3 已入库" assert "艺术家:周杰伦" in message.text def test_message_without_template_config_stays_unchanged( notification_templates: SystemConfigOper, ) -> None: """ 数据库中没有模板配置时消息应保持原样,不应渲染也不应报错。 """ notification_templates.set(SystemConfigKey.NotificationTemplates, None) message = Notification(ctype=ContentType.OrganizeSuccess) MessageTemplateHelper.render(message, **MUSIC_CONTEXT) assert message.title is None assert message.text is None def test_v3_migration_overwrites_templates_once( notification_templates: SystemConfigOper, ) -> None: """ V3 大版本迁移应无条件覆盖用户旧通知模板配置,并写入全部 4 类模板。 """ migration_path = ( Path(__file__).resolve().parent.parent / "database" / "versions" / "4dadad1d161a_3_0_0.py" ) spec = importlib.util.spec_from_file_location("v3_migration", migration_path) migration = importlib.util.module_from_spec(spec) spec.loader.exec_module(migration) notification_templates.set( SystemConfigKey.NotificationTemplates, {"organizeSuccess": "用户自定义模板"} ) migration.upgrade() templates = notification_templates.get(SystemConfigKey.NotificationTemplates) assert set(templates.keys()) == { "organizeSuccess", "downloadAdded", "subscribeAdded", "subscribeComplete", } assert templates["organizeSuccess"] != "用户自定义模板" assert "音乐" in templates["organizeSuccess"] assert "音乐" in templates["downloadAdded"]