- Added tests for OAuth provider admin and consent endpoints covering edge cases. - Extended agent-related tests to handle incorrect project associations and lifecycle state transitions. - Introduced tests for sprint status transitions and validation checks. - Improved multiline formatting consistency across all test functions.
439 lines
16 KiB
Python
439 lines
16 KiB
Python
# 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)
|