"""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")