test: P3-1 第42波单元测试(tts_job/voice_clone use_cases + feature_flags) #824

Merged
xiaoxia merged 1 commits from test/unit-test-wave42 into develop 2026-07-24 16:44:37 +08:00
3 changed files with 837 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
"""Feature Flags 单元测试"""
from __future__ import annotations
import pytest
from packages.infrastructure.feature_flags import (
FeatureFlag,
FeatureFlags,
FeatureScope,
)
class TestFeatureFlag:
"""单个 FeatureFlag 测试"""
def test_default_enabled(self):
"""默认全局启用"""
flag = FeatureFlag(name="test_feature")
assert flag.is_enabled() is True
def test_global_disabled(self):
"""全局禁用"""
flag = FeatureFlag(name="test_feature", global_enabled=False)
assert flag.is_enabled() is False
def test_plan_override_free_disabled(self):
"""free 套餐被覆盖为禁用"""
flag = FeatureFlag(
name="test_feature", global_enabled=True, plan_overrides={"free": False}
)
assert flag.is_enabled(user_plan="free") is False
assert flag.is_enabled(user_plan="basic") is True
assert flag.is_enabled(user_plan="premium") is True
def test_plan_override_premium_only(self):
"""仅 premium 可用"""
flag = FeatureFlag(
name="test_feature",
global_enabled=True,
plan_overrides={"free": False, "basic": False},
)
assert flag.is_enabled(user_plan="free") is False
assert flag.is_enabled(user_plan="basic") is False
assert flag.is_enabled(user_plan="premium") is True
def test_user_override_priority_higher_than_plan(self):
"""用户白名单优先级高于套餐"""
flag = FeatureFlag(
name="test_feature",
global_enabled=False,
plan_overrides={"free": False},
user_overrides={"user_001": True},
)
# 用户在白名单中,即使全局禁用+free套餐也启用
assert flag.is_enabled(user_plan="free", user_id="user_001") is True
def test_user_override_disable(self):
"""用户白名单可单独禁用"""
flag = FeatureFlag(
name="test_feature",
global_enabled=True,
user_overrides={"user_002": False},
)
assert flag.is_enabled(user_id="user_002") is False
assert flag.is_enabled(user_id="user_001") is True
def test_user_override_without_plan(self):
"""用户白名单无需套餐也生效"""
flag = FeatureFlag(
name="test_feature", global_enabled=False, user_overrides={"u1": True}
)
assert flag.is_enabled(user_id="u1") is True
assert flag.is_enabled(user_id="u2") is False
def test_no_plan_uses_global(self):
"""不传 user_plan 时回退到全局开关"""
flag = FeatureFlag(name="test", global_enabled=True, plan_overrides={"free": False})
assert flag.is_enabled() is True
def test_default_values(self):
"""默认值正确"""
flag = FeatureFlag(name="test")
assert flag.name == "test"
assert flag.description == ""
assert flag.global_enabled is True
assert flag.plan_overrides == {}
assert flag.user_overrides == {}
class TestFeatureFlags:
"""FeatureFlags 管理器测试"""
def test_singleton_default_flags(self):
"""默认有 5 个 feature flags"""
ff = FeatureFlags()
flags = ff.list_flags()
assert len(flags) == 5
assert FeatureScope.AI_VOICE_GENERATION in flags
assert FeatureScope.DEDUPLICATION_REPORT in flags
assert FeatureScope.BATCH_EXPORT in flags
assert FeatureScope.MULTI_PLATFORM_OUTPUT in flags
assert FeatureScope.RECIPE_REUSE in flags
def test_is_enabled_existing_flag(self):
"""已存在的 flag 正常判断"""
ff = FeatureFlags()
assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION) is True
def test_is_enabled_nonexistent_flag(self):
"""不存在的 flag 默认禁用"""
ff = FeatureFlags()
assert ff.is_enabled("nonexistent_flag") is False
def test_is_enabled_with_plan(self):
"""按套餐判断"""
ff = FeatureFlags()
# free 套餐 AI 配音不可用
assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION, user_plan="free") is False
assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION, user_plan="basic") is True
assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION, user_plan="premium") is True
def test_is_enabled_premium_only_features(self):
"""仅 premium 可用的功能"""
ff = FeatureFlags()
for feat in [FeatureScope.DEDUPLICATION_REPORT, FeatureScope.MULTI_PLATFORM_OUTPUT]:
assert ff.is_enabled(feat, user_plan="free") is False
assert ff.is_enabled(feat, user_plan="basic") is False
assert ff.is_enabled(feat, user_plan="premium") is True
def test_register_new_flag(self):
"""注册新 flag"""
ff = FeatureFlags()
new_flag = FeatureFlag(name="new_feature", description="新功能", global_enabled=True)
ff.register(new_flag)
assert ff.is_enabled("new_feature") is True
assert ff.get("new_feature") is not None
assert ff.get("new_feature").description == "新功能"
def test_register_overwrites_existing(self):
"""注册同名 flag 覆盖旧的"""
ff = FeatureFlags()
original = ff.get(FeatureScope.AI_VOICE_GENERATION)
assert original.global_enabled is True
new_flag = FeatureFlag(name=FeatureScope.AI_VOICE_GENERATION, global_enabled=False)
ff.register(new_flag)
assert ff.is_enabled(FeatureScope.AI_VOICE_GENERATION) is False
def test_get_returns_none_for_missing(self):
"""获取不存在的 flag 返回 None"""
ff = FeatureFlags()
assert ff.get("no_such_flag") is None
def test_set_global(self):
"""设置全局开关"""
ff = FeatureFlags()
ff.set_global(FeatureScope.BATCH_EXPORT, False)
assert ff.is_enabled(FeatureScope.BATCH_EXPORT) is False
ff.set_global(FeatureScope.BATCH_EXPORT, True)
assert ff.is_enabled(FeatureScope.BATCH_EXPORT) is True
def test_set_global_missing_raises(self):
"""设置不存在的 flag 抛 KeyError"""
ff = FeatureFlags()
with pytest.raises(KeyError):
ff.set_global("nonexistent", True)
def test_set_plan_override(self):
"""设置套餐级别覆盖"""
ff = FeatureFlags()
ff.set_plan_override(FeatureScope.BATCH_EXPORT, "basic", False)
assert ff.is_enabled(FeatureScope.BATCH_EXPORT, user_plan="basic") is False
assert ff.is_enabled(FeatureScope.BATCH_EXPORT, user_plan="premium") is True
def test_set_plan_override_missing_raises(self):
"""设置不存在的 flag 抛 KeyError"""
ff = FeatureFlags()
with pytest.raises(KeyError):
ff.set_plan_override("nonexistent", "free", False)
def test_set_user_override(self):
"""设置用户白名单"""
ff = FeatureFlags()
ff.set_user_override(FeatureScope.DEDUPLICATION_REPORT, "user_42", True)
assert ff.is_enabled(
FeatureScope.DEDUPLICATION_REPORT, user_plan="free", user_id="user_42"
) is True
def test_set_user_override_disable(self):
"""用户白名单禁用"""
ff = FeatureFlags()
ff.set_user_override(FeatureScope.AI_VOICE_GENERATION, "user_99", False)
assert ff.is_enabled(
FeatureScope.AI_VOICE_GENERATION, user_plan="premium", user_id="user_99"
) is False
def test_set_user_override_missing_raises(self):
"""设置不存在的 flag 抛 KeyError"""
ff = FeatureFlags()
with pytest.raises(KeyError):
ff.set_user_override("nonexistent", "u1", True)
def test_list_flags_returns_copy(self):
"""list_flags 返回副本,修改不影响内部"""
ff = FeatureFlags()
flags = ff.list_flags()
flags["new_one"] = FeatureFlag(name="new_one")
assert ff.get("new_one") is None
def test_get_enabled_for_plan_free(self):
"""获取 free 套餐下启用的功能"""
ff = FeatureFlags()
enabled = ff.get_enabled_for_plan("free")
# free 套餐只有 recipe_reuse 可用?不对,看看默认配置
# AI_VOICE_GENERATION: free=False
# DEDUPLICATION_REPORT: free=False, basic=False
# BATCH_EXPORT: free=False
# MULTI_PLATFORM_OUTPUT: free=False, basic=False
# RECIPE_REUSE: free=False
# 所以 free 套餐全部禁用?不,recipe_reuse free=False
# 等等,RECIPE_REUSE 的 plan_overrides 是 {"free": False}
# 那对于 free 套餐,返回 False;但全局是 True
# 所以 free 套餐没有任何启用的?不对...
# 让我重新看:global_enabled=Trueplan_overrides={"free": False}
# 那么 free 套餐 is_enabled 是 False,其他套餐是 True
# 所以 free 套餐应该 0 个启用?不对,等等...
# 不,我需要重新检查每个 flag 的 plan_overrides
# AI_VOICE_GENERATION: free=False → free: False, basic/premium: True
# DEDUPLICATION_REPORT: free=False, basic=False → free/basic: False, premium: True
# BATCH_EXPORT: free=False → free: False, basic/premium: True
# MULTI_PLATFORM_OUTPUT: free=False, basic=False → free/basic: False, premium: True
# RECIPE_REUSE: free=False → free: False, basic/premium: True
assert len(enabled) == 0
def test_get_enabled_for_plan_basic(self):
"""获取 basic 套餐下启用的功能"""
ff = FeatureFlags()
enabled = ff.get_enabled_for_plan("basic")
# basic 套餐:AI_VOICE、BATCH_EXPORT、RECIPE_REUSE 可用
# DEDUPLICATION_REPORT、MULTI_PLATFORM_OUTPUT 不可用
assert FeatureScope.AI_VOICE_GENERATION in enabled
assert FeatureScope.BATCH_EXPORT in enabled
assert FeatureScope.RECIPE_REUSE in enabled
assert FeatureScope.DEDUPLICATION_REPORT not in enabled
assert FeatureScope.MULTI_PLATFORM_OUTPUT not in enabled
assert len(enabled) == 3
def test_get_enabled_for_plan_premium(self):
"""获取 premium 套餐下所有功能都启用"""
ff = FeatureFlags()
enabled = ff.get_enabled_for_plan("premium")
assert len(enabled) == 5
+253
View File
@@ -0,0 +1,253 @@
"""TTS Job Use Cases 单元测试"""
from __future__ import annotations
import pytest
from unittest.mock import MagicMock
from packages.application.tts_job.use_cases import (
CreateTTSJobUseCase,
ListTTSJobsUseCase,
GetTTSJobUseCase,
GetTTSJobStatusUseCase,
DeleteTTSJobUseCase,
)
from packages.application.tts_job.exceptions import TTSJobNotFoundError
from packages.domain.tts_job import TTSJob, TTSJobStatus
@pytest.fixture
def mock_repo():
return MagicMock()
@pytest.fixture
def sample_job():
return TTSJob.create(
user_id="user_001",
input_text="测试文本",
voice_id="voice_001",
voice_model="cosyvoice",
project_id="proj_001",
sample_rate=22050,
format="mp3",
max_retries=3,
)
class TestCreateTTSJobUseCase:
"""创建 TTS 任务用例测试"""
def test_create_success(self, mock_repo, sample_job):
"""创建成功"""
mock_repo.create.return_value = sample_job
use_case = CreateTTSJobUseCase(mock_repo)
result = use_case.execute(
user_id="user_001",
input_text="测试文本",
voice_id="voice_001",
voice_model="cosyvoice",
project_id="proj_001",
)
assert result is not None
assert result.user_id == "user_001"
assert result.input_text == "测试文本"
assert result.status == TTSJobStatus.PENDING
mock_repo.create.assert_called_once()
def test_create_with_default_params(self, mock_repo):
"""使用默认参数创建"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateTTSJobUseCase(mock_repo)
result = use_case.execute(user_id="user_001", input_text="hello")
assert result.voice_id == ""
assert result.voice_model == ""
assert result.sample_rate == 22050
assert result.format == "mp3"
assert result.max_retries == 3
def test_create_with_metadata(self, mock_repo):
"""创建时携带 metadata"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateTTSJobUseCase(mock_repo)
result = use_case.execute(
user_id="user_001",
input_text="test",
metadata={"source": "api", "priority": "high"},
)
assert result.metadata["source"] == "api"
assert result.metadata["priority"] == "high"
def test_create_with_voice_clone_profile(self, mock_repo):
"""使用音色克隆档案创建"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateTTSJobUseCase(mock_repo)
result = use_case.execute(
user_id="user_001",
input_text="test",
voice_clone_profile_id="clone_001",
)
assert result.voice_clone_profile_id == "clone_001"
class TestListTTSJobsUseCase:
"""列出 TTS 任务用例测试"""
def test_list_success(self, mock_repo, sample_job):
"""列出任务成功"""
mock_repo.list_by_user.return_value = [sample_job]
mock_repo.count_by_user.return_value = 1
use_case = ListTTSJobsUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001")
assert len(items) == 1
assert total == 1
mock_repo.list_by_user.assert_called_once_with(
"user_001", status=None, limit=50, offset=0
)
def test_list_with_status_filter(self, mock_repo):
"""按状态过滤"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListTTSJobsUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001", status="completed")
assert total == 0
mock_repo.list_by_user.assert_called_once_with(
"user_001", status="completed", limit=50, offset=0
)
mock_repo.count_by_user.assert_called_once_with(
"user_001", status="completed"
)
def test_list_with_pagination(self, mock_repo):
"""分页参数正确传递"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListTTSJobsUseCase(mock_repo)
use_case.execute(user_id="user_001", skip=10, limit=20)
mock_repo.list_by_user.assert_called_once_with(
"user_001", status=None, limit=20, offset=10
)
def test_list_empty(self, mock_repo):
"""空列表"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListTTSJobsUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001")
assert items == []
assert total == 0
class TestGetTTSJobUseCase:
"""获取 TTS 任务详情用例测试"""
def test_get_success(self, mock_repo, sample_job):
"""获取成功"""
mock_repo.get.return_value = sample_job
use_case = GetTTSJobUseCase(mock_repo)
result = use_case.execute(job_id=sample_job.id, user_id="user_001")
assert result.id == sample_job.id
mock_repo.get.assert_called_once_with(sample_job.id)
def test_get_not_found(self, mock_repo):
"""任务不存在"""
mock_repo.get.return_value = None
use_case = GetTTSJobUseCase(mock_repo)
with pytest.raises(TTSJobNotFoundError):
use_case.execute(job_id="nonexistent", user_id="user_001")
def test_get_wrong_user(self, mock_repo, sample_job):
"""用户不匹配"""
mock_repo.get.return_value = sample_job # user_001
use_case = GetTTSJobUseCase(mock_repo)
with pytest.raises(TTSJobNotFoundError):
use_case.execute(job_id=sample_job.id, user_id="other_user")
class TestGetTTSJobStatusUseCase:
"""查询 TTS 任务状态用例测试"""
def test_get_status_success(self, mock_repo, sample_job):
"""获取状态成功"""
mock_repo.get.return_value = sample_job
use_case = GetTTSJobStatusUseCase(mock_repo)
result = use_case.execute(job_id=sample_job.id, user_id="user_001")
assert result.status == TTSJobStatus.PENDING
def test_get_status_not_found(self, mock_repo):
"""任务不存在抛异常"""
mock_repo.get.return_value = None
use_case = GetTTSJobStatusUseCase(mock_repo)
with pytest.raises(TTSJobNotFoundError):
use_case.execute(job_id="nonexistent", user_id="user_001")
def test_get_status_wrong_user(self, mock_repo, sample_job):
"""用户不匹配抛异常"""
mock_repo.get.return_value = sample_job
use_case = GetTTSJobStatusUseCase(mock_repo)
with pytest.raises(TTSJobNotFoundError):
use_case.execute(job_id=sample_job.id, user_id="other_user")
class TestDeleteTTSJobUseCase:
"""删除 TTS 任务用例测试"""
def test_delete_success(self, mock_repo, sample_job):
"""删除成功"""
mock_repo.get.return_value = sample_job
mock_repo.delete.return_value = True
use_case = DeleteTTSJobUseCase(mock_repo)
result = use_case.execute(job_id=sample_job.id, user_id="user_001")
assert result is True
mock_repo.delete.assert_called_once_with(sample_job.id)
def test_delete_not_found(self, mock_repo):
"""任务不存在返回 False"""
mock_repo.get.return_value = None
use_case = DeleteTTSJobUseCase(mock_repo)
result = use_case.execute(job_id="nonexistent", user_id="user_001")
assert result is False
mock_repo.delete.assert_not_called()
def test_delete_wrong_user(self, mock_repo, sample_job):
"""用户不匹配返回 False"""
mock_repo.get.return_value = sample_job
use_case = DeleteTTSJobUseCase(mock_repo)
result = use_case.execute(job_id=sample_job.id, user_id="other_user")
assert result is False
mock_repo.delete.assert_not_called()
+328
View File
@@ -0,0 +1,328 @@
"""Voice Clone Use Cases 单元测试"""
from __future__ import annotations
import pytest
from unittest.mock import MagicMock
from packages.application.voice_clone.use_cases import (
CreateVoiceCloneUseCase,
ListVoiceClonesUseCase,
GetVoiceCloneUseCase,
GetVoiceCloneStatusUseCase,
DeleteVoiceCloneUseCase,
RetryVoiceCloneUseCase,
VoiceCloneNotFoundError,
VoiceCloneNotRetryableError,
)
from packages.domain.voice_clone_profile import VoiceCloneProfile, VoiceCloneStatus
@pytest.fixture
def mock_repo():
return MagicMock()
@pytest.fixture
def sample_profile():
return VoiceCloneProfile.create(
user_id="user_001",
name="我的音色",
description="测试音色克隆",
source_audio_url="https://example.com/audio.wav",
voice_model="cosyvoice",
language="zh-CN",
gender="female",
max_retries=3,
)
class TestCreateVoiceCloneUseCase:
"""创建音色克隆用例测试"""
def test_create_success(self, mock_repo, sample_profile):
"""创建成功"""
mock_repo.create.return_value = sample_profile
use_case = CreateVoiceCloneUseCase(mock_repo)
result = use_case.execute(
user_id="user_001",
name="我的音色",
source_audio_url="https://example.com/audio.wav",
)
assert result is not None
assert result.user_id == "user_001"
assert result.name == "我的音色"
assert result.status == VoiceCloneStatus.PENDING
mock_repo.create.assert_called_once()
def test_create_with_default_params(self, mock_repo):
"""使用默认参数创建"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateVoiceCloneUseCase(mock_repo)
result = use_case.execute(user_id="user_001", name="测试音色")
assert result.description == ""
assert result.source_audio_url == ""
assert result.voice_model == ""
assert result.language == "zh-CN"
assert result.gender == "unknown"
assert result.max_retries == 3
def test_create_with_metadata(self, mock_repo):
"""创建时携带 metadata"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateVoiceCloneUseCase(mock_repo)
result = use_case.execute(
user_id="user_001",
name="test",
metadata={"source": "upload", "duration": 10},
)
assert result.metadata["source"] == "upload"
assert result.metadata["duration"] == 10
def test_create_custom_max_retries(self, mock_repo):
"""自定义重试次数"""
mock_repo.create.side_effect = lambda x: x
use_case = CreateVoiceCloneUseCase(mock_repo)
result = use_case.execute(
user_id="user_001", name="test", max_retries=5
)
assert result.max_retries == 5
class TestListVoiceClonesUseCase:
"""列出音色克隆用例测试"""
def test_list_success(self, mock_repo, sample_profile):
"""列出成功"""
mock_repo.list_by_user.return_value = [sample_profile]
mock_repo.count_by_user.return_value = 1
use_case = ListVoiceClonesUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001")
assert len(items) == 1
assert total == 1
mock_repo.list_by_user.assert_called_once_with(
"user_001", status=None, limit=50, offset=0
)
def test_list_with_status_filter(self, mock_repo):
"""按状态过滤"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListVoiceClonesUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001", status="completed")
assert total == 0
mock_repo.list_by_user.assert_called_once_with(
"user_001", status="completed", limit=50, offset=0
)
def test_list_with_pagination(self, mock_repo):
"""分页参数正确传递"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListVoiceClonesUseCase(mock_repo)
use_case.execute(user_id="user_001", skip=20, limit=10)
mock_repo.list_by_user.assert_called_once_with(
"user_001", status=None, limit=10, offset=20
)
def test_list_empty(self, mock_repo):
"""空列表"""
mock_repo.list_by_user.return_value = []
mock_repo.count_by_user.return_value = 0
use_case = ListVoiceClonesUseCase(mock_repo)
items, total = use_case.execute(user_id="user_001")
assert items == []
assert total == 0
class TestGetVoiceCloneUseCase:
"""获取音色克隆详情用例测试"""
def test_get_success(self, mock_repo, sample_profile):
"""获取成功"""
mock_repo.get.return_value = sample_profile
use_case = GetVoiceCloneUseCase(mock_repo)
result = use_case.execute(clone_id=sample_profile.id, user_id="user_001")
assert result.id == sample_profile.id
mock_repo.get.assert_called_once_with(sample_profile.id)
def test_get_not_found(self, mock_repo):
"""不存在抛异常"""
mock_repo.get.return_value = None
use_case = GetVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id="nonexistent", user_id="user_001")
def test_get_wrong_user(self, mock_repo, sample_profile):
"""用户不匹配抛异常"""
mock_repo.get.return_value = sample_profile
use_case = GetVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id=sample_profile.id, user_id="other_user")
class TestGetVoiceCloneStatusUseCase:
"""查询音色克隆状态用例测试"""
def test_get_status_success(self, mock_repo, sample_profile):
"""获取状态成功"""
mock_repo.get.return_value = sample_profile
use_case = GetVoiceCloneStatusUseCase(mock_repo)
result = use_case.execute(clone_id=sample_profile.id, user_id="user_001")
assert result.status == VoiceCloneStatus.PENDING
def test_get_status_not_found(self, mock_repo):
"""不存在抛异常"""
mock_repo.get.return_value = None
use_case = GetVoiceCloneStatusUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id="nonexistent", user_id="user_001")
def test_get_status_wrong_user(self, mock_repo, sample_profile):
"""用户不匹配抛异常"""
mock_repo.get.return_value = sample_profile
use_case = GetVoiceCloneStatusUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id=sample_profile.id, user_id="other_user")
class TestDeleteVoiceCloneUseCase:
"""删除音色克隆用例测试"""
def test_delete_success(self, mock_repo, sample_profile):
"""删除成功"""
mock_repo.get.return_value = sample_profile
mock_repo.delete.return_value = True
use_case = DeleteVoiceCloneUseCase(mock_repo)
result = use_case.execute(clone_id=sample_profile.id, user_id="user_001")
assert result is True
mock_repo.delete.assert_called_once_with(sample_profile.id)
def test_delete_not_found(self, mock_repo):
"""不存在返回 False"""
mock_repo.get.return_value = None
use_case = DeleteVoiceCloneUseCase(mock_repo)
result = use_case.execute(clone_id="nonexistent", user_id="user_001")
assert result is False
mock_repo.delete.assert_not_called()
def test_delete_wrong_user(self, mock_repo, sample_profile):
"""用户不匹配返回 False"""
mock_repo.get.return_value = sample_profile
use_case = DeleteVoiceCloneUseCase(mock_repo)
result = use_case.execute(clone_id=sample_profile.id, user_id="other_user")
assert result is False
mock_repo.delete.assert_not_called()
class TestRetryVoiceCloneUseCase:
"""重试音色克隆用例测试"""
def test_retry_success(self, mock_repo, sample_profile):
"""失败状态重试成功"""
sample_profile.status = VoiceCloneStatus.FAILED
sample_profile.retry_count = 1
mock_repo.get.return_value = sample_profile
mock_repo.update.side_effect = lambda x: x
use_case = RetryVoiceCloneUseCase(mock_repo)
result = use_case.execute(clone_id=sample_profile.id, user_id="user_001")
assert result.status == VoiceCloneStatus.PENDING
assert result.retry_count == 2
mock_repo.update.assert_called_once()
def test_retry_not_found(self, mock_repo):
"""不存在抛异常"""
mock_repo.get.return_value = None
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id="nonexistent", user_id="user_001")
def test_retry_wrong_user(self, mock_repo, sample_profile):
"""用户不匹配抛异常"""
sample_profile.status = VoiceCloneStatus.FAILED
mock_repo.get.return_value = sample_profile
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotFoundError):
use_case.execute(clone_id=sample_profile.id, user_id="other_user")
def test_retry_not_retryable_pending(self, mock_repo, sample_profile):
"""pending 状态不可重试"""
sample_profile.status = VoiceCloneStatus.PENDING
mock_repo.get.return_value = sample_profile
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotRetryableError):
use_case.execute(clone_id=sample_profile.id, user_id="user_001")
def test_retry_not_retryable_processing(self, mock_repo, sample_profile):
"""processing 状态不可重试"""
sample_profile.status = VoiceCloneStatus.PROCESSING
mock_repo.get.return_value = sample_profile
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotRetryableError):
use_case.execute(clone_id=sample_profile.id, user_id="user_001")
def test_retry_not_retryable_ready(self, mock_repo, sample_profile):
"""ready 状态不可重试"""
sample_profile.status = VoiceCloneStatus.READY
mock_repo.get.return_value = sample_profile
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotRetryableError):
use_case.execute(clone_id=sample_profile.id, user_id="user_001")
def test_retry_max_retries_exceeded(self, mock_repo, sample_profile):
"""超过重试上限不可重试"""
sample_profile.status = VoiceCloneStatus.FAILED
sample_profile.retry_count = 3
sample_profile.max_retries = 3
mock_repo.get.return_value = sample_profile
use_case = RetryVoiceCloneUseCase(mock_repo)
with pytest.raises(VoiceCloneNotRetryableError):
use_case.execute(clone_id=sample_profile.id, user_id="user_001")