from __future__ import annotations import uuid import pytest from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession from cpv3.modules.files.models import File from cpv3.modules.projects.models import Project from cpv3.modules.tasks.schemas import TaskSubmitResponse from cpv3.modules.tasks.service import TaskService from cpv3.modules.users.models import User pytest.importorskip("greenlet") @pytest.fixture async def workflow_project(test_db_session: AsyncSession, test_user: User) -> Project: project = Project( id=uuid.uuid4(), owner_id=test_user.id, name="Workflow Project", description="Typed workflow test project", language="ru", status="DRAFT", is_active=True, ) test_db_session.add(project) await test_db_session.commit() await test_db_session.refresh(project) return project @pytest.fixture async def source_file( test_db_session: AsyncSession, test_user: User, workflow_project: Project, ) -> File: file = File( id=uuid.uuid4(), owner_id=test_user.id, project_id=workflow_project.id, original_filename="source.mp4", path="users/test/source.mp4", storage_backend="S3", mime_type="video/mp4", size_bytes=1024, file_format="mp4", is_uploaded=True, is_deleted=False, ) test_db_session.add(file) await test_db_session.commit() await test_db_session.refresh(file) return file class TestProjectWorkspaceEndpoints: async def test_get_workspace_returns_default_state( self, auth_client: AsyncClient, workflow_project: Project, ) -> None: response = await auth_client.get(f"/api/projects/{workflow_project.id}/workspace") assert response.status_code == 200 data = response.json() assert data["project_id"] == str(workflow_project.id) assert data["revision"] == 0 assert data["version"] == 1 assert data["phase"] == "INGEST" assert data["current_screen"] == "upload" assert data["source_file_id"] is None assert data["active_job"] is None assert data["workspace_view"] == { "used_file_ids": [], "selected_file_id": None, } async def test_get_workspace_forbidden_for_other_users_project( self, auth_client: AsyncClient, test_db_session: AsyncSession, other_user: User, ) -> None: foreign_project = Project( id=uuid.uuid4(), owner_id=other_user.id, name="Other Project", description=None, language="ru", status="DRAFT", is_active=True, ) test_db_session.add(foreign_project) await test_db_session.commit() response = await auth_client.get(f"/api/projects/{foreign_project.id}/workspace") assert response.status_code == 403 async def test_set_source_file_action_updates_workspace( self, auth_client: AsyncClient, workflow_project: Project, source_file: File, ) -> None: response = await auth_client.post( f"/api/projects/{workflow_project.id}/workflow/actions", json={ "type": "SET_SOURCE_FILE", "revision": 0, "file_id": str(source_file.id), }, ) assert response.status_code == 200 data = response.json() assert data["revision"] == 1 assert data["phase"] == "VERIFY" assert data["current_screen"] == "verify" assert data["source_file_id"] == str(source_file.id) assert data["workspace_view"] == { "used_file_ids": [str(source_file.id)], "selected_file_id": str(source_file.id), } async def test_action_returns_conflict_on_stale_revision( self, auth_client: AsyncClient, workflow_project: Project, source_file: File, ) -> None: first_response = await auth_client.post( f"/api/projects/{workflow_project.id}/workflow/actions", json={ "type": "SET_SOURCE_FILE", "revision": 0, "file_id": str(source_file.id), }, ) assert first_response.status_code == 200 response = await auth_client.post( f"/api/projects/{workflow_project.id}/workflow/actions", json={ "type": "RESET_SOURCE_FILE", "revision": 0, }, ) assert response.status_code == 409 async def test_start_media_convert_action_sets_active_job( self, auth_client: AsyncClient, workflow_project: Project, source_file: File, monkeypatch: pytest.MonkeyPatch, ) -> None: async def fake_submit_media_convert( self, *, requester: User, request, ) -> TaskSubmitResponse: assert requester.id == workflow_project.owner_id assert request.file_key == source_file.path assert request.project_id == workflow_project.id return TaskSubmitResponse( job_id=uuid.UUID("00000000-0000-4000-a000-000000000123"), webhook_url=("http://test/api/tasks/webhook/00000000-0000-4000-a000-000000000123/"), status="PENDING", ) monkeypatch.setattr( TaskService, "submit_media_convert", fake_submit_media_convert, ) set_source_response = await auth_client.post( f"/api/projects/{workflow_project.id}/workflow/actions", json={ "type": "SET_SOURCE_FILE", "revision": 0, "file_id": str(source_file.id), }, ) assert set_source_response.status_code == 200 response = await auth_client.post( f"/api/projects/{workflow_project.id}/workflow/actions", json={ "type": "START_MEDIA_CONVERT", "revision": 1, "output_format": "mp4", "out_folder": "output_files", }, ) assert response.status_code == 200 data = response.json() assert data["revision"] == 2 assert data["phase"] == "VERIFY" assert data["current_screen"] == "verify" assert data["active_job"] == { "job_id": "00000000-0000-4000-a000-000000000123", "job_type": "MEDIA_CONVERT", }