b4e3bb0fe7
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 47s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 50s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m22s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m27s
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 / Validate - Type Check (mypy) (push) Successful in 3m31s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m4s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m31s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m24s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m21s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 8m19s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m24s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m4s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m25s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m21s
CI/CD Pipeline / Integration Tests (push) Successful in 3m28s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m27s
CI/CD Pipeline / Unit Tests (push) Successful in 15m5s
CI/CD Pipeline / CI Gate (push) Has been skipped
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 / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Successful in 14m25s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 9s
148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
"""Tests for EditPlanService.replace_all_clips_transactional."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import MagicMock, PropertyMock, patch
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
|
|
|
|
|
|
class TestReplaceAllClipsTransactional:
|
|
"""事务性替换片段方法测试。"""
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_success_commits_once(self, mock_clip_cls, mock_model_cls):
|
|
"""成功时单次 commit,不 rollback。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
# Mock query chain for delete
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 3
|
|
db.query.return_value = query_mock
|
|
|
|
# Mock query chain for mark_ready (pending_with_asset)
|
|
# After the create loop, query returns empty list (no pending clips with asset)
|
|
ready_query = MagicMock()
|
|
ready_query.filter.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
db.query.side_effect = [query_mock, ready_query]
|
|
|
|
# Mock EditPlanClip.create to return a mock entity
|
|
mock_entity = MagicMock()
|
|
mock_entity.id = "clip-1"
|
|
mock_entity.plan_id = "plan-1"
|
|
mock_entity.clip_type = "main"
|
|
mock_entity.order = 0
|
|
mock_entity.asset_id = "asset-1"
|
|
mock_entity.text_content = ""
|
|
mock_entity.start_time = 0.0
|
|
mock_entity.duration = 3.0
|
|
mock_entity.transition_effect = "cut"
|
|
mock_entity.transition_duration = 0.0
|
|
mock_entity.playback_speed = 1.0
|
|
mock_entity.status.value = "pending"
|
|
mock_entity.config = {}
|
|
mock_clip_cls.create.return_value = mock_entity
|
|
|
|
# Mock the model constructor
|
|
mock_model_instance = MagicMock()
|
|
mock_model_cls.return_value = mock_model_instance
|
|
|
|
# Mock clip_repo
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
result = svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "asset-1", "start_time": 0.0, "duration": 3.0, "order": 0}],
|
|
)
|
|
|
|
assert result == 1
|
|
db.commit.assert_called_once()
|
|
db.rollback.assert_not_called()
|
|
db.add.assert_called_once_with(mock_model_instance)
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_failure_rolls_back(self, mock_clip_cls, mock_model_cls):
|
|
"""异常时自动 rollback。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 0
|
|
db.query.return_value = query_mock
|
|
|
|
# Simulate failure during create
|
|
mock_clip_cls.create.side_effect = ValueError("模拟异常")
|
|
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
with pytest.raises(ValueError, match="模拟异常"):
|
|
svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "bad", "start_time": 0.0, "duration": 1.0, "order": 0}],
|
|
)
|
|
|
|
db.rollback.assert_called_once()
|
|
db.commit.assert_not_called()
|
|
|
|
@patch("packages.adapters.sqlalchemy_impl.models.EditPlanClipModel")
|
|
@patch("app.services.edit_plan_service.EditPlanClip")
|
|
def test_order_defaults_to_index(self, mock_clip_cls, mock_model_cls):
|
|
"""order=0 时使用索引值作为 order。"""
|
|
from app.services.edit_plan_service import EditPlanService
|
|
|
|
db = MagicMock()
|
|
query_mock = MagicMock()
|
|
query_mock.filter.return_value.delete.return_value = 0
|
|
db.query.return_value = query_mock
|
|
|
|
ready_query = MagicMock()
|
|
ready_query.filter.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
db.query.side_effect = [query_mock, ready_query]
|
|
|
|
mock_entity = MagicMock()
|
|
mock_entity.id = "clip-1"
|
|
mock_entity.plan_id = "plan-1"
|
|
mock_entity.clip_type = "main"
|
|
mock_entity.order = 0 # order=0 → 使用 i=0
|
|
mock_entity.asset_id = "a1"
|
|
mock_entity.text_content = ""
|
|
mock_entity.start_time = 0.0
|
|
mock_entity.duration = 1.0
|
|
mock_entity.transition_effect = "cut"
|
|
mock_entity.transition_duration = 0.0
|
|
mock_entity.playback_speed = 1.0
|
|
mock_entity.status.value = "pending"
|
|
mock_entity.config = {}
|
|
mock_clip_cls.create.return_value = mock_entity
|
|
|
|
mock_model_cls.return_value = MagicMock()
|
|
|
|
clip_repo = MagicMock()
|
|
clip_repo.session = db
|
|
|
|
svc = EditPlanService.__new__(EditPlanService)
|
|
svc._clip_repo = clip_repo
|
|
|
|
svc.replace_all_clips_transactional(
|
|
"plan-1",
|
|
[{"asset_id": "a1", "start_time": 0.0, "duration": 1.0, "order": 0}],
|
|
)
|
|
|
|
# order=0 → falsy → use index i=0
|
|
create_call = mock_clip_cls.create.call_args
|
|
assert create_call.kwargs["order"] == 0
|