"""查重 UseCase 单元测试.""" from __future__ import annotations from unittest.mock import MagicMock import pytest from packages.application.duplication import ( DeleteDuplicationRecordUseCase, GetDuplicationDetailUseCase, ListDuplicationRecordsUseCase, RetryDuplicationUseCase, UploadForDuplicationCommand, UploadForDuplicationUseCase, ) from packages.domain.duplication import DuplicationRecord @pytest.fixture def mock_repo(): return MagicMock() @pytest.fixture def sample_record(): r = DuplicationRecord.create( user_id="user_1", filename="test_video.mp4", file_size=1048576, storage_key="uploads/test_video.mp4", duration_seconds=30.5, ) r.id = "dup_123" return r @pytest.fixture def failed_record(): r = DuplicationRecord.create( user_id="user_1", filename="failed.mp4", file_size=512000, storage_key="uploads/failed.mp4", ) r.id = "dup_456" r.mark_failed("网络超时") return r class TestUploadForDuplicationUseCase: """UploadForDuplicationUseCase 测试""" def test_upload_success(self, mock_repo, sample_record): """上传查重成功""" mock_repo.create.return_value = sample_record use_case = UploadForDuplicationUseCase(mock_repo) command = UploadForDuplicationCommand( user_id="user_1", filename="test_video.mp4", file_size=1048576, storage_key="uploads/test_video.mp4", duration_seconds=30.5, ) result = use_case.execute(command) assert result.id == "dup_123" assert result.user_id == "user_1" mock_repo.create.assert_called_once() created = mock_repo.create.call_args[0][0] assert isinstance(created, DuplicationRecord) assert created.status == "pending" def test_upload_default_duration(self, mock_repo): """不传 duration 默认 0.0""" mock_repo.create.side_effect = lambda x: x use_case = UploadForDuplicationUseCase(mock_repo) command = UploadForDuplicationCommand( user_id="user_1", filename="test.mp4", file_size=1000, storage_key="key", ) result = use_case.execute(command) assert result.duration_seconds == 0.0 def test_upload_empty_user_id_raises(self, mock_repo): """空 user_id 在 domain 层抛出""" use_case = UploadForDuplicationUseCase(mock_repo) command = UploadForDuplicationCommand( user_id="", filename="test.mp4", file_size=1000, storage_key="key", ) with pytest.raises(ValueError, match="user_id cannot be empty"): use_case.execute(command) mock_repo.create.assert_not_called() def test_upload_zero_file_size_raises(self, mock_repo): """文件大小为0抛出""" use_case = UploadForDuplicationUseCase(mock_repo) command = UploadForDuplicationCommand( user_id="user_1", filename="test.mp4", file_size=0, storage_key="key", ) with pytest.raises(ValueError, match="file_size must be positive"): use_case.execute(command) class TestListDuplicationRecordsUseCase: """ListDuplicationRecordsUseCase 测试""" def test_list_returns_results(self, mock_repo, sample_record): """正常返回用户查重记录列表""" mock_repo.list_by_user.return_value = [sample_record] use_case = ListDuplicationRecordsUseCase(mock_repo) result = use_case.execute("user_1") assert len(result) == 1 assert result[0].id == "dup_123" mock_repo.list_by_user.assert_called_once_with("user_1", offset=0, limit=50) def test_list_with_offset_limit(self, mock_repo, sample_record): """带 offset 和 limit 参数""" mock_repo.list_by_user.return_value = [sample_record] use_case = ListDuplicationRecordsUseCase(mock_repo) use_case.execute("user_1", offset=10, limit=20) mock_repo.list_by_user.assert_called_once_with("user_1", offset=10, limit=20) def test_empty_user_id_raises(self, mock_repo): """空 user_id 抛出""" use_case = ListDuplicationRecordsUseCase(mock_repo) with pytest.raises(ValueError, match="user_id 不能为空"): use_case.execute("") mock_repo.list_by_user.assert_not_called() def test_whitespace_user_id_raises(self, mock_repo): """纯空格 user_id 抛出""" use_case = ListDuplicationRecordsUseCase(mock_repo) with pytest.raises(ValueError, match="user_id 不能为空"): use_case.execute(" ") def test_user_id_stripped(self, mock_repo, sample_record): """user_id 被 strip""" mock_repo.list_by_user.return_value = [sample_record] use_case = ListDuplicationRecordsUseCase(mock_repo) use_case.execute(" user_1 ") mock_repo.list_by_user.assert_called_once_with("user_1", offset=0, limit=50) class TestGetDuplicationDetailUseCase: """GetDuplicationDetailUseCase 测试""" def test_get_existing(self, mock_repo, sample_record): """获取存在的记录""" mock_repo.get.return_value = sample_record use_case = GetDuplicationDetailUseCase(mock_repo) result = use_case.execute("dup_123") assert result is not None assert result.id == "dup_123" mock_repo.get.assert_called_once_with("dup_123") def test_get_nonexistent_returns_none(self, mock_repo): """获取不存在的记录返回 None""" mock_repo.get.return_value = None use_case = GetDuplicationDetailUseCase(mock_repo) result = use_case.execute("nonexistent") assert result is None class TestDeleteDuplicationRecordUseCase: """DeleteDuplicationRecordUseCase 测试""" def test_delete_success(self, mock_repo): """删除成功""" mock_repo.delete.return_value = True use_case = DeleteDuplicationRecordUseCase(mock_repo) result = use_case.execute("dup_123") assert result is True mock_repo.delete.assert_called_once_with("dup_123") def test_delete_nonexistent_returns_false(self, mock_repo): """删除不存在的记录返回 False""" mock_repo.delete.return_value = False use_case = DeleteDuplicationRecordUseCase(mock_repo) result = use_case.execute("nonexistent") assert result is False class TestRetryDuplicationUseCase: """RetryDuplicationUseCase 测试""" def test_retry_failed_record(self, mock_repo, failed_record): """失败记录可以重试,状态重置为 pending""" mock_repo.get.return_value = failed_record mock_repo.update.side_effect = lambda x: x use_case = RetryDuplicationUseCase(mock_repo) result = use_case.execute("dup_456") assert result is not None assert result.status == "pending" assert result.error_message == "" assert result.duplicate_rate is None assert result.duplicate_count == 0 assert result.segments == [] mock_repo.update.assert_called_once() def test_retry_nonexistent_returns_none(self, mock_repo): """重试不存在的记录返回 None""" mock_repo.get.return_value = None use_case = RetryDuplicationUseCase(mock_repo) result = use_case.execute("nonexistent") assert result is None mock_repo.update.assert_not_called() def test_retry_pending_raises(self, mock_repo, sample_record): """pending 状态的记录不能重试""" assert sample_record.status == "pending" mock_repo.get.return_value = sample_record use_case = RetryDuplicationUseCase(mock_repo) with pytest.raises(ValueError, match="只有 failed 状态的记录可以重试"): use_case.execute("dup_123") mock_repo.update.assert_not_called() def test_retry_completed_raises(self, mock_repo, sample_record): """completed 状态的记录不能重试""" sample_record.mark_completed(duplicate_rate=25.5, duplicate_count=3, segments=[]) mock_repo.get.return_value = sample_record use_case = RetryDuplicationUseCase(mock_repo) with pytest.raises(ValueError, match="只有 failed 状态的记录可以重试"): use_case.execute("dup_123") mock_repo.update.assert_not_called() class TestUploadForDuplicationCommand: """UploadForDuplicationCommand 数据类测试""" def test_command_fields(self): """命令对象字段正确""" cmd = UploadForDuplicationCommand( user_id="user_1", filename="test.mp4", file_size=1024, storage_key="uploads/test.mp4", duration_seconds=15.0, ) assert cmd.user_id == "user_1" assert cmd.filename == "test.mp4" assert cmd.file_size == 1024 assert cmd.storage_key == "uploads/test.mp4" assert cmd.duration_seconds == 15.0 def test_default_duration(self): """duration_seconds 默认 0.0""" cmd = UploadForDuplicationCommand( user_id="user_1", filename="test.mp4", file_size=1024, storage_key="key", ) assert cmd.duration_seconds == 0.0 def test_command_is_dataclass(self): """是 dataclass""" from dataclasses import is_dataclass assert is_dataclass(UploadForDuplicationCommand)