test: P3-1 第42波单元测试(tts_job/voice_clone use_cases + feature_flags) #824
Executable
+256
@@ -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=True,plan_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
|
||||
Executable
+253
@@ -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()
|
||||
Executable
+328
@@ -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")
|
||||
Reference in New Issue
Block a user