# tests/crud/syndarix/test_issue_crud.py """ Tests for Issue CRUD operations. """ import uuid from datetime import UTC, datetime import pytest from app.crud.syndarix import issue as issue_crud from app.models.syndarix import IssuePriority, IssueStatus, SyncStatus from app.schemas.syndarix import IssueCreate, IssueUpdate class TestIssueCreate: """Tests for issue creation.""" @pytest.mark.asyncio async def test_create_issue_success(self, async_test_db, test_project_crud): """Test successfully creating an issue.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Test Issue", body="This is a test issue body", status=IssueStatus.OPEN, priority=IssuePriority.HIGH, labels=["bug", "security"], story_points=5, ) result = await issue_crud.create(session, obj_in=issue_data) assert result.id is not None assert result.title == "Test Issue" assert result.body == "This is a test issue body" assert result.status == IssueStatus.OPEN assert result.priority == IssuePriority.HIGH assert result.labels == ["bug", "security"] assert result.story_points == 5 @pytest.mark.asyncio async def test_create_issue_with_external_tracker(self, async_test_db, test_project_crud): """Test creating issue with external tracker info.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="External Issue", external_tracker="gitea", external_id="gitea-123", external_url="https://gitea.example.com/issues/123", external_number=123, ) result = await issue_crud.create(session, obj_in=issue_data) assert result.external_tracker == "gitea" assert result.external_id == "gitea-123" assert result.external_number == 123 @pytest.mark.asyncio async def test_create_issue_minimal(self, async_test_db, test_project_crud): """Test creating issue with minimal fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Minimal Issue", ) result = await issue_crud.create(session, obj_in=issue_data) assert result.title == "Minimal Issue" assert result.body == "" # Default assert result.status == IssueStatus.OPEN # Default assert result.priority == IssuePriority.MEDIUM # Default class TestIssueRead: """Tests for issue read operations.""" @pytest.mark.asyncio async def test_get_issue_by_id(self, async_test_db, test_issue_crud): """Test getting issue by ID.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.get(session, id=str(test_issue_crud.id)) assert result is not None assert result.id == test_issue_crud.id @pytest.mark.asyncio async def test_get_issue_by_id_not_found(self, async_test_db): """Test getting non-existent issue returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.get(session, id=str(uuid.uuid4())) assert result is None @pytest.mark.asyncio async def test_get_with_details(self, async_test_db, test_issue_crud): """Test getting issue with related details.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.get_with_details( session, issue_id=test_issue_crud.id, ) assert result is not None assert result["issue"].id == test_issue_crud.id assert result["project_name"] is not None class TestIssueUpdate: """Tests for issue update operations.""" @pytest.mark.asyncio async def test_update_issue_basic_fields(self, async_test_db, test_issue_crud): """Test updating basic issue fields.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue = await issue_crud.get(session, id=str(test_issue_crud.id)) update_data = IssueUpdate( title="Updated Title", body="Updated body content", ) result = await issue_crud.update(session, db_obj=issue, obj_in=update_data) assert result.title == "Updated Title" assert result.body == "Updated body content" @pytest.mark.asyncio async def test_update_issue_status(self, async_test_db, test_issue_crud): """Test updating issue status.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue = await issue_crud.get(session, id=str(test_issue_crud.id)) update_data = IssueUpdate(status=IssueStatus.IN_PROGRESS) result = await issue_crud.update(session, db_obj=issue, obj_in=update_data) assert result.status == IssueStatus.IN_PROGRESS @pytest.mark.asyncio async def test_update_issue_priority(self, async_test_db, test_issue_crud): """Test updating issue priority.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue = await issue_crud.get(session, id=str(test_issue_crud.id)) update_data = IssueUpdate(priority=IssuePriority.CRITICAL) result = await issue_crud.update(session, db_obj=issue, obj_in=update_data) assert result.priority == IssuePriority.CRITICAL @pytest.mark.asyncio async def test_update_issue_labels(self, async_test_db, test_issue_crud): """Test updating issue labels.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issue = await issue_crud.get(session, id=str(test_issue_crud.id)) update_data = IssueUpdate(labels=["new-label", "updated"]) result = await issue_crud.update(session, db_obj=issue, obj_in=update_data) assert "new-label" in result.labels class TestIssueAssignment: """Tests for issue assignment operations.""" @pytest.mark.asyncio async def test_assign_to_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): """Test assigning issue to an agent.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.assign_to_agent( session, issue_id=test_issue_crud.id, agent_id=test_agent_instance_crud.id, ) assert result is not None assert result.assigned_agent_id == test_agent_instance_crud.id assert result.human_assignee is None @pytest.mark.asyncio async def test_unassign_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): """Test unassigning agent from issue.""" _test_engine, AsyncTestingSessionLocal = async_test_db # First assign async with AsyncTestingSessionLocal() as session: await issue_crud.assign_to_agent( session, issue_id=test_issue_crud.id, agent_id=test_agent_instance_crud.id, ) # Then unassign async with AsyncTestingSessionLocal() as session: result = await issue_crud.assign_to_agent( session, issue_id=test_issue_crud.id, agent_id=None, ) assert result.assigned_agent_id is None @pytest.mark.asyncio async def test_assign_to_human(self, async_test_db, test_issue_crud): """Test assigning issue to a human.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.assign_to_human( session, issue_id=test_issue_crud.id, human_assignee="developer@example.com", ) assert result is not None assert result.human_assignee == "developer@example.com" assert result.assigned_agent_id is None @pytest.mark.asyncio async def test_assign_to_human_clears_agent(self, async_test_db, test_issue_crud, test_agent_instance_crud): """Test assigning to human clears agent assignment.""" _test_engine, AsyncTestingSessionLocal = async_test_db # First assign to agent async with AsyncTestingSessionLocal() as session: await issue_crud.assign_to_agent( session, issue_id=test_issue_crud.id, agent_id=test_agent_instance_crud.id, ) # Then assign to human async with AsyncTestingSessionLocal() as session: result = await issue_crud.assign_to_human( session, issue_id=test_issue_crud.id, human_assignee="developer@example.com", ) assert result.human_assignee == "developer@example.com" assert result.assigned_agent_id is None class TestIssueLifecycle: """Tests for issue lifecycle operations.""" @pytest.mark.asyncio async def test_close_issue(self, async_test_db, test_issue_crud): """Test closing an issue.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.close_issue(session, issue_id=test_issue_crud.id) assert result is not None assert result.status == IssueStatus.CLOSED assert result.closed_at is not None @pytest.mark.asyncio async def test_reopen_issue(self, async_test_db, test_project_crud): """Test reopening a closed issue.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create and close an issue async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Issue to Reopen", ) created = await issue_crud.create(session, obj_in=issue_data) await issue_crud.close_issue(session, issue_id=created.id) issue_id = created.id # Reopen async with AsyncTestingSessionLocal() as session: result = await issue_crud.reopen_issue(session, issue_id=issue_id) assert result is not None assert result.status == IssueStatus.OPEN assert result.closed_at is None class TestIssueByProject: """Tests for getting issues by project.""" @pytest.mark.asyncio async def test_get_by_project(self, async_test_db, test_project_crud, test_issue_crud): """Test getting issues by project.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: issues, total = await issue_crud.get_by_project( session, project_id=test_project_crud.id, ) assert total >= 1 assert all(i.project_id == test_project_crud.id for i in issues) @pytest.mark.asyncio async def test_get_by_project_with_status(self, async_test_db, test_project_crud): """Test filtering issues by status.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issues with different statuses async with AsyncTestingSessionLocal() as session: open_issue = IssueCreate( project_id=test_project_crud.id, title="Open Issue Filter", status=IssueStatus.OPEN, ) await issue_crud.create(session, obj_in=open_issue) closed_issue = IssueCreate( project_id=test_project_crud.id, title="Closed Issue Filter", status=IssueStatus.CLOSED, ) await issue_crud.create(session, obj_in=closed_issue) async with AsyncTestingSessionLocal() as session: issues, _ = await issue_crud.get_by_project( session, project_id=test_project_crud.id, status=IssueStatus.OPEN, ) assert all(i.status == IssueStatus.OPEN for i in issues) @pytest.mark.asyncio async def test_get_by_project_with_priority(self, async_test_db, test_project_crud): """Test filtering issues by priority.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: high_issue = IssueCreate( project_id=test_project_crud.id, title="High Priority Issue", priority=IssuePriority.HIGH, ) await issue_crud.create(session, obj_in=high_issue) async with AsyncTestingSessionLocal() as session: issues, _ = await issue_crud.get_by_project( session, project_id=test_project_crud.id, priority=IssuePriority.HIGH, ) assert all(i.priority == IssuePriority.HIGH for i in issues) @pytest.mark.asyncio async def test_get_by_project_with_search(self, async_test_db, test_project_crud): """Test searching issues by title/body.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: searchable_issue = IssueCreate( project_id=test_project_crud.id, title="Searchable Unique Title", body="This body contains searchable content", ) await issue_crud.create(session, obj_in=searchable_issue) async with AsyncTestingSessionLocal() as session: issues, total = await issue_crud.get_by_project( session, project_id=test_project_crud.id, search="Searchable Unique", ) assert total >= 1 assert any(i.title == "Searchable Unique Title" for i in issues) class TestIssueBySprint: """Tests for getting issues by sprint.""" @pytest.mark.asyncio async def test_get_by_sprint(self, async_test_db, test_project_crud, test_sprint_crud): """Test getting issues by sprint.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issue in sprint async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Sprint Issue", sprint_id=test_sprint_crud.id, ) await issue_crud.create(session, obj_in=issue_data) async with AsyncTestingSessionLocal() as session: issues = await issue_crud.get_by_sprint( session, sprint_id=test_sprint_crud.id, ) assert len(issues) >= 1 assert all(i.sprint_id == test_sprint_crud.id for i in issues) class TestIssueSyncStatus: """Tests for issue sync status operations.""" @pytest.mark.asyncio async def test_update_sync_status(self, async_test_db, test_project_crud): """Test updating issue sync status.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issue with external tracker async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Sync Status Issue", external_tracker="gitea", external_id="gitea-456", ) created = await issue_crud.create(session, obj_in=issue_data) issue_id = created.id # Update sync status now = datetime.now(UTC) async with AsyncTestingSessionLocal() as session: result = await issue_crud.update_sync_status( session, issue_id=issue_id, sync_status=SyncStatus.PENDING, last_synced_at=now, ) assert result is not None assert result.sync_status == SyncStatus.PENDING assert result.last_synced_at is not None @pytest.mark.asyncio async def test_get_pending_sync(self, async_test_db, test_project_crud): """Test getting issues pending sync.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issue with pending sync async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="Pending Sync Issue", external_tracker="gitea", external_id="gitea-789", ) created = await issue_crud.create(session, obj_in=issue_data) # Set to pending await issue_crud.update_sync_status( session, issue_id=created.id, sync_status=SyncStatus.PENDING, ) async with AsyncTestingSessionLocal() as session: issues = await issue_crud.get_pending_sync(session) assert any(i.sync_status == SyncStatus.PENDING for i in issues) class TestIssueExternalTracker: """Tests for external tracker operations.""" @pytest.mark.asyncio async def test_get_by_external_id(self, async_test_db, test_project_crud): """Test getting issue by external tracker ID.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issue with external ID async with AsyncTestingSessionLocal() as session: issue_data = IssueCreate( project_id=test_project_crud.id, title="External ID Issue", external_tracker="github", external_id="github-unique-123", ) await issue_crud.create(session, obj_in=issue_data) async with AsyncTestingSessionLocal() as session: result = await issue_crud.get_by_external_id( session, external_tracker="github", external_id="github-unique-123", ) assert result is not None assert result.external_id == "github-unique-123" @pytest.mark.asyncio async def test_get_by_external_id_not_found(self, async_test_db): """Test getting non-existent external ID returns None.""" _test_engine, AsyncTestingSessionLocal = async_test_db async with AsyncTestingSessionLocal() as session: result = await issue_crud.get_by_external_id( session, external_tracker="gitea", external_id="non-existent", ) assert result is None class TestIssueStats: """Tests for issue statistics.""" @pytest.mark.asyncio async def test_get_project_stats(self, async_test_db, test_project_crud): """Test getting issue statistics for a project.""" _test_engine, AsyncTestingSessionLocal = async_test_db # Create issues with various statuses and priorities async with AsyncTestingSessionLocal() as session: for status in [IssueStatus.OPEN, IssueStatus.IN_PROGRESS, IssueStatus.CLOSED]: issue_data = IssueCreate( project_id=test_project_crud.id, title=f"Stats Issue {status.value}", status=status, story_points=3, ) await issue_crud.create(session, obj_in=issue_data) async with AsyncTestingSessionLocal() as session: stats = await issue_crud.get_project_stats( session, project_id=test_project_crud.id, ) assert "total" in stats assert "open" in stats assert "in_progress" in stats assert "closed" in stats assert "by_priority" in stats assert "total_story_points" in stats