forked from cardosofelipe/fast-next-template
Infrastructure: - Add Redis and Celery workers to all docker-compose files - Fix celery migration race condition in entrypoint.sh - Add healthchecks and resource limits to dev compose - Update .env.template with Redis/Celery variables Backend Models & Schemas: - Rename Sprint.completed_points to velocity (per requirements) - Add AgentInstance.name as required field - Rename Issue external tracker fields for consistency - Add IssueSource and TrackerType enums - Add Project.default_tracker_type field Backend Fixes: - Add Celery retry configuration with exponential backoff - Remove unused sequence counter from EventBus - Add mypy overrides for test dependencies - Fix test file using wrong schema (UserUpdate -> dict) Frontend Fixes: - Fix memory leak in useProjectEvents (proper cleanup) - Fix race condition with stale closure in reconnection - Sync TokenWithUser type with regenerated API client - Fix expires_in null handling in useAuth - Clean up unused imports in prototype pages - Add ESLint relaxed rules for prototype files CI/CD: - Add E2E testing stage with Testcontainers - Add security scanning with Trivy and pip-audit - Add dependency caching for faster builds Tests: - Update all tests to use renamed fields (velocity, name, etc.) - Fix 14 schema test failures - All 1500 tests pass with 91% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
410 lines
16 KiB
Python
410 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)
|