"""Job Use Cases 单元测试""" from __future__ import annotations from unittest.mock import MagicMock import pytest from packages.application.jobs import ( CancelJobUseCase, CompleteJobCommand, CompleteJobUseCase, CreateJobCommand, CreateJobUseCase, FailJobCommand, FailJobUseCase, GetJobStatisticsUseCase, GetJobUseCase, ListJobsUseCase, RetryJobUseCase, SubmitJobUseCase, UpdateJobProgressCommand, UpdateJobProgressUseCase, ) from packages.domain.job import Job, JobStatus, JobType @pytest.fixture def mock_repo(): return MagicMock() @pytest.fixture def sample_job(): return Job.create( project_id="proj_001", job_type=JobType.VIDEO_COMPOSE, payload={"template_id": "tpl_001"}, source_id="src_001", created_by_user_id="user_001", max_retries=3, ) class TestCreateJobCommand: """CreateJobCommand 测试""" def test_default_values(self): cmd = CreateJobCommand(project_id="p1", job_type=JobType.VIDEO_COMPOSE) assert cmd.project_id == "p1" assert cmd.payload == {} assert cmd.source_id == "" assert cmd.created_by_user_id == "" assert cmd.max_retries == 3 class TestCreateJobUseCase: """CreateJobUseCase 测试""" def test_create_success(self, mock_repo, sample_job): mock_repo.create.return_value = sample_job use_case = CreateJobUseCase(mock_repo) cmd = CreateJobCommand( project_id="proj_001", job_type=JobType.VIDEO_COMPOSE, payload={"template_id": "tpl_001"}, source_id="src_001", created_by_user_id="user_001", max_retries=3, ) result = use_case.execute(cmd) assert result.status == JobStatus.PENDING assert result.project_id == "proj_001" mock_repo.create.assert_called_once() def test_create_with_string_job_type(self, mock_repo): mock_repo.create.side_effect = lambda x: x use_case = CreateJobUseCase(mock_repo) cmd = CreateJobCommand(project_id="p1", job_type="video_compose") result = use_case.execute(cmd) assert result.job_type == JobType.VIDEO_COMPOSE class TestSubmitJobUseCase: """SubmitJobUseCase 测试""" def test_submit_success(self, mock_repo, sample_job): mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = SubmitJobUseCase(mock_repo) result = use_case.execute(sample_job.id, celery_task_id="celery_123") assert result.status == JobStatus.RUNNING assert result.celery_task_id == "celery_123" def test_submit_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = SubmitJobUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute("nonexistent") def test_submit_wrong_status(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job use_case = SubmitJobUseCase(mock_repo) with pytest.raises(ValueError, match="只有 pending"): use_case.execute(sample_job.id) class TestUpdateJobProgressUseCase: """UpdateJobProgressUseCase 测试""" def test_update_progress_success(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = UpdateJobProgressUseCase(mock_repo) cmd = UpdateJobProgressCommand(job_id=sample_job.id, progress=50.0, current_stage="渲染中") result = use_case.execute(cmd) assert result.progress == 50.0 assert result.current_stage == "渲染中" def test_update_progress_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = UpdateJobProgressUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute(UpdateJobProgressCommand(job_id="x", progress=10)) def test_update_progress_wrong_status(self, mock_repo, sample_job): sample_job.status = JobStatus.PENDING mock_repo.get.return_value = sample_job use_case = UpdateJobProgressUseCase(mock_repo) with pytest.raises(ValueError, match="只有 running"): use_case.execute(UpdateJobProgressCommand(job_id=sample_job.id, progress=10)) class TestCompleteJobUseCase: """CompleteJobUseCase 测试""" def test_complete_from_running(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = CompleteJobUseCase(mock_repo) cmd = CompleteJobCommand(job_id=sample_job.id, result={"url": "http://..."}) result = use_case.execute(cmd) assert result.status == JobStatus.SUCCESS assert result.result["url"] == "http://..." def test_complete_from_pending(self, mock_repo, sample_job): sample_job.status = JobStatus.PENDING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = CompleteJobUseCase(mock_repo) result = use_case.execute(CompleteJobCommand(job_id=sample_job.id)) assert result.status == JobStatus.SUCCESS def test_complete_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = CompleteJobUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute(CompleteJobCommand(job_id="x")) def test_complete_failed_status_raises(self, mock_repo, sample_job): sample_job.status = JobStatus.FAILED mock_repo.get.return_value = sample_job use_case = CompleteJobUseCase(mock_repo) with pytest.raises(ValueError, match="只有 running/pending"): use_case.execute(CompleteJobCommand(job_id=sample_job.id)) class TestFailJobUseCase: """FailJobUseCase 测试""" def test_fail_success(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = FailJobUseCase(mock_repo) cmd = FailJobCommand(job_id=sample_job.id, error_message="渲染失败") result = use_case.execute(cmd) assert result.status == JobStatus.FAILED assert "渲染失败" in result.error_message def test_fail_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = FailJobUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute(FailJobCommand(job_id="x", error_message="err")) def test_fail_updates_error_message(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = FailJobUseCase(mock_repo) result = use_case.execute(FailJobCommand(job_id=sample_job.id, error_message="连接超时")) assert result.error_message == "连接超时" class TestRetryJobUseCase: """RetryJobUseCase 测试""" def test_retry_success(self, mock_repo, sample_job): sample_job.status = JobStatus.FAILED sample_job.retry_count = 1 mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = RetryJobUseCase(mock_repo) result = use_case.execute(sample_job.id) assert result.status == JobStatus.PENDING assert result.retry_count == 2 def test_retry_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = RetryJobUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute("nonexistent") class TestCancelJobUseCase: """CancelJobUseCase 测试""" def test_cancel_pending(self, mock_repo, sample_job): mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = CancelJobUseCase(mock_repo) result = use_case.execute(sample_job.id) assert result.status == JobStatus.CANCELLED def test_cancel_running(self, mock_repo, sample_job): sample_job.status = JobStatus.RUNNING mock_repo.get.return_value = sample_job mock_repo.update.side_effect = lambda x: x use_case = CancelJobUseCase(mock_repo) result = use_case.execute(sample_job.id) assert result.status == JobStatus.CANCELLED def test_cancel_terminal_raises(self, mock_repo, sample_job): sample_job.status = JobStatus.SUCCESS mock_repo.get.return_value = sample_job use_case = CancelJobUseCase(mock_repo) with pytest.raises(ValueError, match="终态"): use_case.execute(sample_job.id) def test_cancel_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = CancelJobUseCase(mock_repo) with pytest.raises(ValueError, match="任务不存在"): use_case.execute("nonexistent") class TestGetJobUseCase: """GetJobUseCase 测试""" def test_get_exists(self, mock_repo, sample_job): mock_repo.get.return_value = sample_job use_case = GetJobUseCase(mock_repo) result = use_case.execute(sample_job.id) assert result.id == sample_job.id def test_get_not_found(self, mock_repo): mock_repo.get.return_value = None use_case = GetJobUseCase(mock_repo) result = use_case.execute("nonexistent") assert result is None class TestListJobsUseCase: """ListJobsUseCase 测试""" def test_list_by_project(self, mock_repo, sample_job): mock_repo.list_by_project.return_value = [sample_job] use_case = ListJobsUseCase(mock_repo) result = use_case.execute(project_id="proj_001") assert len(result) == 1 mock_repo.list_by_project.assert_called_once() def test_list_by_user(self, mock_repo, sample_job): mock_repo.list_by_user.return_value = [sample_job] use_case = ListJobsUseCase(mock_repo) result = use_case.execute(user_id="user_001") assert len(result) == 1 mock_repo.list_by_user.assert_called_once() def test_list_no_filter_raises(self, mock_repo): use_case = ListJobsUseCase(mock_repo) with pytest.raises(ValueError, match="必须指定"): use_case.execute() def test_list_with_filters(self, mock_repo, sample_job): mock_repo.list_by_project.return_value = [sample_job] use_case = ListJobsUseCase(mock_repo) use_case.execute( project_id="p1", job_type=JobType.VIDEO_COMPOSE, status=JobStatus.RUNNING, limit=20, offset=10, ) mock_repo.list_by_project.assert_called_once_with( "p1", job_type=JobType.VIDEO_COMPOSE, status=JobStatus.RUNNING, limit=20, offset=10 ) class TestGetJobStatisticsUseCase: """GetJobStatisticsUseCase 测试""" def test_statistics(self, mock_repo): mock_repo.count_by_project.side_effect = [10, 2, 3, 4, 1] use_case = GetJobStatisticsUseCase(mock_repo) stats = use_case.execute("proj_001") assert stats["project_id"] == "proj_001" assert stats["total"] == 10 assert stats["pending"] == 2 assert stats["running"] == 3 assert stats["success"] == 4 assert stats["failed"] == 1