Files
xiaoxia-saas/tests/unit/test_strenum_deserialization.py
T
CI Bot 1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
chore(backend): Phase 3 清理 — 未使用依赖删除 + pyflakes 警告清零 + 测试文件冗余清理
1. 未使用依赖清理:
   - 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL

2. pyflakes 警告清零 (apps/ + packages/ + tests/):
   - 移除 17 处未使用的 import (F401)
   - 修复 26 处未使用的局部变量 (F841):
     * 有副作用的赋值转为裸调用
     * 无副作用的赋值直接删除
   - 修复 1 处未使用的异常变量 (F841)
   - 修复 1 处空 except 块

3. 测试文件冗余清理:
   - 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
   - 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 16:14:46 +08:00

250 lines
9.9 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.
"""StrEnum 反序列化单元测试。
验证 SQLAlchemy 仓储从 DB 读取数据后,实体的 status 字段是正确的 StrEnum 类型,
而不是纯字符串。这是 500 错误的根因修复测试。
根因:SQLAlchemy Column(String(20)) 读出的是 str,直接传给 domain 实体后,
路由层 job.status.value 会抛 AttributeErrorstr 没有 .value 属性)。
"""
from __future__ import annotations
from datetime import datetime, timezone
from unittest.mock import MagicMock
import pytest
from packages.adapters.sqlalchemy_impl.tts_job_repository import SQLAlchemyTTSJobRepository
from packages.adapters.sqlalchemy_impl.voice_clone_profile_repository import SQLAlchemyVoiceCloneProfileRepository
from packages.domain.generation_task import GenerationTaskStatus
from packages.domain.tts_job import TTSJobStatus
from packages.domain.voice_clone_profile import VoiceCloneStatus
def _make_tts_model(status: str = "pending"):
"""模拟 SQLAlchemy TTSJobModelstatus 是纯字符串)。"""
model = MagicMock()
model.id = "job_001"
model.user_id = "user_001"
model.input_text = "测试文本"
model.voice_id = "voice_001"
model.voice_model = ""
model.project_id = ""
model.voice_clone_profile_id = ""
model.status = status # 纯字符串,模拟 DB 读取
model.output_audio_url = ""
model.output_audio_key = ""
model.duration = 0.0
model.file_size = 0
model.sample_rate = 22050
model.format = "mp3"
model.error_message = ""
model.retry_count = 0
model.max_retries = 3
model.metadata_ = {}
model.started_at = None
model.completed_at = None
model.created_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(timezone.utc)
return model
def _make_voice_clone_model(status: str = "pending"):
"""模拟 SQLAlchemy VoiceCloneProfileModelstatus 是纯字符串)。"""
model = MagicMock()
model.id = "profile_001"
model.user_id = "user_001"
model.name = "测试音色"
model.description = ""
model.source_audio_url = "https://example.com/audio.wav"
model.voice_id = ""
model.voice_model = ""
model.language = "zh-CN"
model.gender = "unknown"
model.status = status # 纯字符串,模拟 DB 读取
model.error_message = ""
model.retry_count = 0
model.max_retries = 3
model.metadata_ = {}
model.created_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(timezone.utc)
return model
def _make_generation_task_model(status: str = "pending"):
"""模拟 SQLAlchemy GenerationTaskModelstatus 是纯字符串)。"""
model = MagicMock()
model.id = "task_001"
model.project_id = "project_001"
model.strategy_id = "strategy_001"
model.asset_library_id = "lib_001"
model.voice_library_id = "voice_lib_001"
model.template_id = "template_001"
model.asset_ids = []
model.title_ids = []
model.voice_ids = []
model.status = status # 纯字符串,模拟 DB 读取
model.progress = 0.0
model.result_count = 0
model.error_message = ""
model.started_at = None
model.completed_at = None
model.created_by_user_id = "user_001"
model.source_edit_plan_id = ""
model.created_at = datetime.now(timezone.utc)
return model
class TestTTSJobStatusDeserialization:
"""TTSJob 仓储:DB 读出的 status 字符串应转为 TTSJobStatus 枚举。"""
@pytest.mark.parametrize("status_str", ["pending", "processing", "completed", "failed", "cancelled"])
def test_model_to_entity_converts_status_to_enum(self, status_str: str) -> None:
"""_model_to_entity 将纯字符串 status 转为 TTSJobStatus 枚举。"""
model = _make_tts_model(status=status_str)
entity = SQLAlchemyTTSJobRepository._model_to_entity(model)
assert isinstance(
entity.status, TTSJobStatus
), f"status 应该是 TTSJobStatus 枚举,实际是 {type(entity.status).__name__}"
assert entity.status.value == status_str
def test_status_value_attribute_works(self) -> None:
"""路由层 job.status.value 不再抛 AttributeError。"""
model = _make_tts_model(status="processing")
entity = SQLAlchemyTTSJobRepository._model_to_entity(model)
# 这是之前 500 错误的直接原因
assert entity.status.value == "processing"
def test_status_string_comparison_still_works(self) -> None:
"""StrEnum 继承自 str,字符串比较仍然兼容。"""
model = _make_tts_model(status="completed")
entity = SQLAlchemyTTSJobRepository._model_to_entity(model)
assert entity.status == "completed" # StrEnum 与 str 可比较
assert entity.status == TTSJobStatus.COMPLETED
class TestVoiceCloneStatusDeserialization:
"""VoiceCloneProfile 仓储:DB 读出的 status 字符串应转为 VoiceCloneStatus 枚举。"""
@pytest.mark.parametrize("status_str", ["pending", "processing", "ready", "failed", "disabled"])
def test_model_to_entity_converts_status_to_enum(self, status_str: str) -> None:
"""_model_to_entity 将纯字符串 status 转为 VoiceCloneStatus 枚举。"""
model = _make_voice_clone_model(status=status_str)
entity = SQLAlchemyVoiceCloneProfileRepository._model_to_entity(model)
assert isinstance(
entity.status, VoiceCloneStatus
), f"status 应该是 VoiceCloneStatus 枚举,实际是 {type(entity.status).__name__}"
assert entity.status.value == status_str
def test_status_value_attribute_works(self) -> None:
"""profile.status.value 不再抛 AttributeError。"""
model = _make_voice_clone_model(status="processing")
entity = SQLAlchemyVoiceCloneProfileRepository._model_to_entity(model)
assert entity.status.value == "processing"
class TestGenerationTaskStatusDeserialization:
"""GenerationTask 仓储:DB 读出的 status 字符串应转为 GenerationTaskStatus 枚举。"""
@pytest.mark.parametrize("status_str", ["pending", "running", "completed", "failed", "cancelled"])
def test_to_domain_converts_status_to_enum(self, status_str: str) -> None:
"""_to_domain 将纯字符串 status 转为 GenerationTaskStatus 枚举。"""
model = _make_generation_task_model(status=status_str)
# _to_domain 是模块级函数,直接导入使用
from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain
entity = _to_domain(model)
assert isinstance(
entity.status, GenerationTaskStatus
), f"status 应该是 GenerationTaskStatus 枚举,实际是 {type(entity.status).__name__}"
assert entity.status.value == status_str
def test_status_value_attribute_works(self) -> None:
"""task.status.value 不再抛 AttributeError。"""
model = _make_generation_task_model(status="running")
from packages.adapters.sqlalchemy_impl.generation_task_repository import _to_domain
entity = _to_domain(model)
assert entity.status.value == "running"
class TestAlreadyCorrectRepositories:
"""验证已经正确转换枚举的仓储(回归测试,防止退化)。"""
def test_edit_plan_status_converted(self) -> None:
"""EditPlan 仓储已正确转换 status。"""
from packages.adapters.sqlalchemy_impl.edit_plan_repository import SQLAlchemyEditPlanRepository
from packages.domain.edit_plan import EditPlanStatus
model = MagicMock()
model.id = "plan_001"
model.template_id = "template_001"
model.name = "测试计划"
model.status = "editing" # 纯字符串
model.total_duration = 0.0
model.source_edit_plan_id = ""
model.project_id = ""
model.created_by_user_id = "user_001"
model.config = {}
model.created_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(timezone.utc)
entity = SQLAlchemyEditPlanRepository._model_to_entity(model)
assert isinstance(entity.status, EditPlanStatus)
assert entity.status.value == "editing"
def test_edit_template_status_converted(self) -> None:
"""EditTemplate 仓储已正确转换 status。"""
from packages.adapters.sqlalchemy_impl.edit_template_repository import SQLAlchemyEditTemplateRepository
from packages.domain.edit_template import EditTemplateStatus
model = MagicMock()
model.id = "template_001"
model.name = "测试模板"
model.description = ""
model.template_type = "default"
model.config = {}
model.preview_url = ""
model.sort_weight = 0
model.status = "active" # 纯字符串
model.created_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(timezone.utc)
entity = SQLAlchemyEditTemplateRepository._model_to_entity(model)
assert isinstance(entity.status, EditTemplateStatus)
assert entity.status.value == "active"
def test_edit_plan_clip_status_converted(self) -> None:
"""EditPlanClip 仓储已正确转换 status。"""
from packages.adapters.sqlalchemy_impl.edit_plan_clip_repository import SQLAlchemyEditPlanClipRepository
from packages.domain.edit_plan_clip import EditPlanClipStatus
model = MagicMock()
model.id = "clip_001"
model.plan_id = "plan_001"
model.clip_type = "main"
model.order = 0
model.template_clip_config_id = ""
model.asset_id = ""
model.text_content = ""
model.start_time = 0.0
model.duration = 0.0
model.transition_effect = "cut"
model.status = "ready" # 纯字符串
model.config = {}
model.created_at = datetime.now(timezone.utc)
model.updated_at = datetime.now(timezone.utc)
entity = SQLAlchemyEditPlanClipRepository._model_to_entity(model)
assert isinstance(entity.status, EditPlanClipStatus)
assert entity.status.value == "ready"