Files
xiaoxia-saas/tests/unit/test_strenum_deserialization.py
T
xiaoxia 53fb25efcf
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 19s
CI/CD Pipeline / Build Staging API Image (push) Successful in 41s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m17s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m30s
CI/CD Pipeline / Validate - Style (push) Successful in 4m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 59s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m33s
CI/CD Pipeline / Validate - Security (push) Successful in 7m12s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m38s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
CI/CD Pipeline / Unit Tests (push) Successful in 10m11s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Failing after 26h14m3s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 26h24m21s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 26h19m47s
CI/CD Pipeline / PR Build Web Image (push) Failing after 26h23m44s
CI/CD Pipeline / PR Build API Image (push) Failing after 26h23m44s
CI/CD Pipeline / Deploy Production (push) Failing after 26h13m23s
CI/CD Pipeline / Build Production Web Image (push) Failing after 26h13m26s
CI/CD Pipeline / CI Gate (push) Failing after 26h13m25s
CI/CD Pipeline / Build Production API Image (push) Failing after 26h13m26s
CI/CD Pipeline / Canary Release to Production (push) Failing after 26h13m23s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 26h19m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 26h23m37s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 26h23m45s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 26h19m46s
fix(#1834): 批量修复 UP 系列静态分析警告(UP007/UP006/UP017/UP035) (#1928)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-15 12:59:17 +08:00

250 lines
9.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.
"""StrEnum 反序列化单元测试。
验证 SQLAlchemy 仓储从 DB 读取数据后,实体的 status 字段是正确的 StrEnum 类型,
而不是纯字符串。这是 500 错误的根因修复测试。
根因:SQLAlchemy Column(String(20)) 读出的是 str,直接传给 domain 实体后,
路由层 job.status.value 会抛 AttributeError(str 没有 .value 属性)。
"""
from __future__ import annotations
from datetime import UTC, 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 TTSJobModel(status 是纯字符串)。"""
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(UTC)
model.updated_at = datetime.now(UTC)
return model
def _make_voice_clone_model(status: str = "pending"):
"""模拟 SQLAlchemy VoiceCloneProfileModel(status 是纯字符串)。"""
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(UTC)
model.updated_at = datetime.now(UTC)
return model
def _make_generation_task_model(status: str = "pending"):
"""模拟 SQLAlchemy GenerationTaskModel(status 是纯字符串)。"""
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(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(UTC)
model.updated_at = datetime.now(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(UTC)
model.updated_at = datetime.now(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(UTC)
model.updated_at = datetime.now(UTC)
entity = SQLAlchemyEditPlanClipRepository._model_to_entity(model)
assert isinstance(entity.status, EditPlanClipStatus)
assert entity.status.value == "ready"