3921a657e8
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 52s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m56s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 54s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m20s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m27s
CI/CD Pipeline / Unit Tests (push) Failing after 8m24s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 16m1s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m14s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m34s
322 lines
11 KiB
Python
Executable File
322 lines
11 KiB
Python
Executable File
"""Voice Clone Use Cases 单元测试"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from packages.application.voice_clone.use_cases import (
|
|
CreateVoiceCloneUseCase,
|
|
DeleteVoiceCloneUseCase,
|
|
GetVoiceCloneStatusUseCase,
|
|
GetVoiceCloneUseCase,
|
|
ListVoiceClonesUseCase,
|
|
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")
|