# tests/crud/syndarix/test_project_crud.py """ Tests for Project CRUD operations. """ import uuid import pytest from app.crud.syndarix import project as project_crud from app.models.syndarix import AutonomyLevel, ProjectStatus from app.schemas.syndarix import ProjectCreate, ProjectUpdate class TestProjectCreate: """Tests for project creation.""" @pytest.mark.asyncio async def test_create_project_success(self, async_test_db, test_owner_crud): """Test successfully creating a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project_data = ProjectCreate( name="New Project", slug="new-project", description="A brand new project", autonomy_level=AutonomyLevel.MILESTONE, status=ProjectStatus.ACTIVE, settings={"key": "value"}, owner_id=test_owner_crud.id, ) result = await project_crud.create(session, obj_in=project_data) assert result.id is not None assert result.name == "New Project" assert result.slug == "new-project" assert result.description == "A brand new project" assert result.autonomy_level == AutonomyLevel.MILESTONE assert result.status == ProjectStatus.ACTIVE assert result.settings == {"key": "value"} assert result.owner_id == test_owner_crud.id @pytest.mark.asyncio async def test_create_project_duplicate_slug_fails(self, async_test_db, test_project_crud): """Test creating project with duplicate slug raises ValueError.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project_data = ProjectCreate( name="Duplicate Project", slug=test_project_crud.slug, # Duplicate slug description="This should fail", ) with pytest.raises(ValueError) as exc_info: await project_crud.create(session, obj_in=project_data) assert "already exists" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_create_project_minimal_fields(self, async_test_db): """Test creating project with minimal required fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project_data = ProjectCreate( name="Minimal Project", slug="minimal-project", ) result = await project_crud.create(session, obj_in=project_data) assert result.name == "Minimal Project" assert result.slug == "minimal-project" assert result.autonomy_level == AutonomyLevel.MILESTONE # Default assert result.status == ProjectStatus.ACTIVE # Default class TestProjectRead: """Tests for project read operations.""" @pytest.mark.asyncio async def test_get_project_by_id(self, async_test_db, test_project_crud): """Test getting project by ID.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.get(session, id=str(test_project_crud.id)) assert result is not None assert result.id == test_project_crud.id assert result.name == test_project_crud.name @pytest.mark.asyncio async def test_get_project_by_id_not_found(self, async_test_db): """Test getting non-existent project returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.get(session, id=str(uuid.uuid4())) assert result is None @pytest.mark.asyncio async def test_get_project_by_slug(self, async_test_db, test_project_crud): """Test getting project by slug.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.get_by_slug(session, slug=test_project_crud.slug) assert result is not None assert result.slug == test_project_crud.slug @pytest.mark.asyncio async def test_get_project_by_slug_not_found(self, async_test_db): """Test getting non-existent slug returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.get_by_slug(session, slug="non-existent-slug") assert result is None class TestProjectUpdate: """Tests for project update operations.""" @pytest.mark.asyncio async def test_update_project_basic_fields(self, async_test_db, test_project_crud): """Test updating basic project fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project = await project_crud.get(session, id=str(test_project_crud.id)) update_data = ProjectUpdate( name="Updated Project Name", description="Updated description", ) result = await project_crud.update(session, db_obj=project, obj_in=update_data) assert result.name == "Updated Project Name" assert result.description == "Updated description" @pytest.mark.asyncio async def test_update_project_status(self, async_test_db, test_project_crud): """Test updating project status.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project = await project_crud.get(session, id=str(test_project_crud.id)) update_data = ProjectUpdate(status=ProjectStatus.PAUSED) result = await project_crud.update(session, db_obj=project, obj_in=update_data) assert result.status == ProjectStatus.PAUSED @pytest.mark.asyncio async def test_update_project_autonomy_level(self, async_test_db, test_project_crud): """Test updating project autonomy level.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project = await project_crud.get(session, id=str(test_project_crud.id)) update_data = ProjectUpdate(autonomy_level=AutonomyLevel.AUTONOMOUS) result = await project_crud.update(session, db_obj=project, obj_in=update_data) assert result.autonomy_level == AutonomyLevel.AUTONOMOUS @pytest.mark.asyncio async def test_update_project_settings(self, async_test_db, test_project_crud): """Test updating project settings.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project = await project_crud.get(session, id=str(test_project_crud.id)) new_settings = {"mcp_servers": ["gitea", "slack"], "webhook_url": "https://example.com"} update_data = ProjectUpdate(settings=new_settings) result = await project_crud.update(session, db_obj=project, obj_in=update_data) assert result.settings == new_settings class TestProjectDelete: """Tests for project delete operations.""" @pytest.mark.asyncio async def test_delete_project(self, async_test_db, test_owner_crud): """Test deleting a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create a project to delete async with AsyncTestingSessionLocal() as session: project_data = ProjectCreate( name="Delete Me", slug="delete-me-project", owner_id=test_owner_crud.id, ) created = await project_crud.create(session, obj_in=project_data) project_id = created.id # Delete the project async with AsyncTestingSessionLocal() as session: result = await project_crud.remove(session, id=str(project_id)) assert result is not None assert result.id == project_id # Verify deletion async with AsyncTestingSessionLocal() as session: deleted = await project_crud.get(session, id=str(project_id)) assert deleted is None @pytest.mark.asyncio async def test_delete_nonexistent_project(self, async_test_db): """Test deleting non-existent project returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.remove(session, id=str(uuid.uuid4())) assert result is None class TestProjectFilters: """Tests for project filtering and search.""" @pytest.mark.asyncio async def test_get_multi_with_filters_status(self, async_test_db, test_owner_crud): """Test filtering projects by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create multiple projects with different statuses async with AsyncTestingSessionLocal() as session: for i, status in enumerate(ProjectStatus): project_data = ProjectCreate( name=f"Project {status.value}", slug=f"project-filter-{status.value}-{i}", status=status, owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=project_data) # Filter by ACTIVE status async with AsyncTestingSessionLocal() as session: projects, total = await project_crud.get_multi_with_filters( session, status=ProjectStatus.ACTIVE, ) assert all(p.status == ProjectStatus.ACTIVE for p in projects) @pytest.mark.asyncio async def test_get_multi_with_filters_search(self, async_test_db, test_owner_crud): """Test searching projects by name/slug.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: project_data = ProjectCreate( name="Searchable Project", slug="searchable-unique-slug", description="This project is searchable", owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=project_data) async with AsyncTestingSessionLocal() as session: projects, total = await project_crud.get_multi_with_filters( session, search="Searchable", ) assert total >= 1 assert any(p.name == "Searchable Project" for p in projects) @pytest.mark.asyncio async def test_get_multi_with_filters_owner(self, async_test_db, test_owner_crud, test_project_crud): """Test filtering projects by owner.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: projects, total = await project_crud.get_multi_with_filters( session, owner_id=test_owner_crud.id, ) assert total >= 1 assert all(p.owner_id == test_owner_crud.id for p in projects) @pytest.mark.asyncio async def test_get_multi_with_filters_pagination(self, async_test_db, test_owner_crud): """Test pagination of project results.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create multiple projects async with AsyncTestingSessionLocal() as session: for i in range(5): project_data = ProjectCreate( name=f"Page Project {i}", slug=f"page-project-{i}", owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=project_data) # Get first page async with AsyncTestingSessionLocal() as session: page1, total = await project_crud.get_multi_with_filters( session, skip=0, limit=2, owner_id=test_owner_crud.id, ) assert len(page1) <= 2 assert total >= 5 @pytest.mark.asyncio async def test_get_multi_with_filters_sorting(self, async_test_db, test_owner_crud): """Test sorting project results.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: for i, name in enumerate(["Charlie", "Alice", "Bob"]): project_data = ProjectCreate( name=name, slug=f"sort-project-{name.lower()}", owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=project_data) async with AsyncTestingSessionLocal() as session: projects, _ = await project_crud.get_multi_with_filters( session, sort_by="name", sort_order="asc", owner_id=test_owner_crud.id, ) names = [p.name for p in projects if p.name in ["Alice", "Bob", "Charlie"]] assert names == sorted(names) class TestProjectSpecialMethods: """Tests for special project CRUD methods.""" @pytest.mark.asyncio async def test_archive_project(self, async_test_db, test_project_crud): """Test archiving a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.archive_project(session, project_id=test_project_crud.id) assert result is not None assert result.status == ProjectStatus.ARCHIVED @pytest.mark.asyncio async def test_archive_nonexistent_project(self, async_test_db): """Test archiving non-existent project returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await project_crud.archive_project(session, project_id=uuid.uuid4()) assert result is None @pytest.mark.asyncio async def test_get_projects_by_owner(self, async_test_db, test_owner_crud, test_project_crud): """Test getting all projects by owner.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: projects = await project_crud.get_projects_by_owner( session, owner_id=test_owner_crud.id, ) assert len(projects) >= 1 assert all(p.owner_id == test_owner_crud.id for p in projects) @pytest.mark.asyncio async def test_get_projects_by_owner_with_status(self, async_test_db, test_owner_crud): """Test getting projects by owner filtered by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create projects with different statuses async with AsyncTestingSessionLocal() as session: active_project = ProjectCreate( name="Active Owner Project", slug="active-owner-project", status=ProjectStatus.ACTIVE, owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=active_project) paused_project = ProjectCreate( name="Paused Owner Project", slug="paused-owner-project", status=ProjectStatus.PAUSED, owner_id=test_owner_crud.id, ) await project_crud.create(session, obj_in=paused_project) async with AsyncTestingSessionLocal() as session: projects = await project_crud.get_projects_by_owner( session, owner_id=test_owner_crud.id, status=ProjectStatus.ACTIVE, ) assert all(p.status == ProjectStatus.ACTIVE for p in projects)