Files
xiaoxia-saas/tests/unit/test_job_use_cases.py
T
CI Bot 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
style: auto-format with black + isort + prettier
2026-07-24 08:47:07 +00:00

317 lines
11 KiB
Python
Executable File

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