- Add Project model with slug, description, autonomy level, and settings - Add AgentType model for agent templates with model config and failover - Add AgentInstance model for running agents with status and memory - Add Issue model with external tracker sync (Gitea/GitHub/GitLab) - Add Sprint model with velocity tracking and lifecycle management - Add comprehensive Pydantic schemas with validation - Add full CRUD operations for all models with filtering/sorting - Add 280+ tests for models, schemas, and CRUD operations Implements #23, #24, #25, #26, #27 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
301 lines
9.5 KiB
Python
301 lines
9.5 KiB
Python
# tests/schemas/syndarix/test_project_schemas.py
|
|
"""
|
|
Tests for Project schema validation.
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from app.schemas.syndarix import (
|
|
AutonomyLevel,
|
|
ProjectCreate,
|
|
ProjectStatus,
|
|
ProjectUpdate,
|
|
)
|
|
|
|
|
|
class TestProjectCreateValidation:
|
|
"""Tests for ProjectCreate schema validation."""
|
|
|
|
def test_valid_project_create(self, valid_project_data):
|
|
"""Test creating project with valid data."""
|
|
project = ProjectCreate(**valid_project_data)
|
|
|
|
assert project.name == "Test Project"
|
|
assert project.slug == "test-project"
|
|
assert project.description == "A test project"
|
|
|
|
def test_project_create_defaults(self):
|
|
"""Test that defaults are applied correctly."""
|
|
project = ProjectCreate(
|
|
name="Minimal Project",
|
|
slug="minimal-project",
|
|
)
|
|
|
|
assert project.autonomy_level == AutonomyLevel.MILESTONE
|
|
assert project.status == ProjectStatus.ACTIVE
|
|
assert project.settings == {}
|
|
assert project.owner_id is None
|
|
|
|
def test_project_create_with_owner(self, valid_project_data):
|
|
"""Test creating project with owner ID."""
|
|
owner_id = uuid.uuid4()
|
|
project = ProjectCreate(
|
|
**valid_project_data,
|
|
owner_id=owner_id,
|
|
)
|
|
|
|
assert project.owner_id == owner_id
|
|
|
|
def test_project_create_name_empty_fails(self):
|
|
"""Test that empty name raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name="",
|
|
slug="valid-slug",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("name" in str(e) for e in errors)
|
|
|
|
def test_project_create_name_whitespace_only_fails(self):
|
|
"""Test that whitespace-only name raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name=" ",
|
|
slug="valid-slug",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("name" in str(e) for e in errors)
|
|
|
|
def test_project_create_name_stripped(self):
|
|
"""Test that name is stripped of leading/trailing whitespace."""
|
|
project = ProjectCreate(
|
|
name=" Padded Name ",
|
|
slug="padded-slug",
|
|
)
|
|
|
|
assert project.name == "Padded Name"
|
|
|
|
def test_project_create_slug_required(self):
|
|
"""Test that slug is required for create."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(name="No Slug Project")
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("slug" in str(e).lower() for e in errors)
|
|
|
|
|
|
class TestProjectSlugValidation:
|
|
"""Tests for Project slug validation."""
|
|
|
|
def test_valid_slugs(self):
|
|
"""Test various valid slug formats."""
|
|
valid_slugs = [
|
|
"simple",
|
|
"with-hyphens",
|
|
"has123numbers",
|
|
"mix3d-with-hyphen5",
|
|
"a", # Single character
|
|
]
|
|
|
|
for slug in valid_slugs:
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug=slug,
|
|
)
|
|
assert project.slug == slug
|
|
|
|
def test_invalid_slug_uppercase(self):
|
|
"""Test that uppercase letters in slug raise ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="Invalid-Uppercase",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("slug" in str(e).lower() for e in errors)
|
|
|
|
def test_invalid_slug_special_chars(self):
|
|
"""Test that special characters in slug raise ValidationError."""
|
|
invalid_slugs = [
|
|
"has_underscore",
|
|
"has.dot",
|
|
"has@symbol",
|
|
"has space",
|
|
"has/slash",
|
|
]
|
|
|
|
for slug in invalid_slugs:
|
|
with pytest.raises(ValidationError):
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug=slug,
|
|
)
|
|
|
|
def test_invalid_slug_starts_with_hyphen(self):
|
|
"""Test that slug starting with hyphen raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="-invalid-start",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("hyphen" in str(e).lower() for e in errors)
|
|
|
|
def test_invalid_slug_ends_with_hyphen(self):
|
|
"""Test that slug ending with hyphen raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="invalid-end-",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("hyphen" in str(e).lower() for e in errors)
|
|
|
|
def test_invalid_slug_consecutive_hyphens(self):
|
|
"""Test that consecutive hyphens in slug raise ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="invalid--consecutive",
|
|
)
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("consecutive" in str(e).lower() for e in errors)
|
|
|
|
|
|
class TestProjectUpdateValidation:
|
|
"""Tests for ProjectUpdate schema validation."""
|
|
|
|
def test_project_update_partial(self):
|
|
"""Test updating only some fields."""
|
|
update = ProjectUpdate(
|
|
name="Updated Name",
|
|
)
|
|
|
|
assert update.name == "Updated Name"
|
|
assert update.slug is None
|
|
assert update.description is None
|
|
assert update.autonomy_level is None
|
|
assert update.status is None
|
|
|
|
def test_project_update_all_fields(self):
|
|
"""Test updating all fields."""
|
|
owner_id = uuid.uuid4()
|
|
update = ProjectUpdate(
|
|
name="Updated Name",
|
|
slug="updated-slug",
|
|
description="Updated description",
|
|
autonomy_level=AutonomyLevel.AUTONOMOUS,
|
|
status=ProjectStatus.PAUSED,
|
|
settings={"key": "value"},
|
|
owner_id=owner_id,
|
|
)
|
|
|
|
assert update.name == "Updated Name"
|
|
assert update.slug == "updated-slug"
|
|
assert update.autonomy_level == AutonomyLevel.AUTONOMOUS
|
|
assert update.status == ProjectStatus.PAUSED
|
|
|
|
def test_project_update_empty_name_fails(self):
|
|
"""Test that empty name in update raises ValidationError."""
|
|
with pytest.raises(ValidationError) as exc_info:
|
|
ProjectUpdate(name="")
|
|
|
|
errors = exc_info.value.errors()
|
|
assert any("name" in str(e) for e in errors)
|
|
|
|
def test_project_update_slug_validation(self):
|
|
"""Test that slug validation applies to updates too."""
|
|
with pytest.raises(ValidationError):
|
|
ProjectUpdate(slug="Invalid-Slug")
|
|
|
|
|
|
class TestProjectEnums:
|
|
"""Tests for Project enum validation."""
|
|
|
|
def test_valid_autonomy_levels(self):
|
|
"""Test all valid autonomy levels."""
|
|
for level in AutonomyLevel:
|
|
# Replace underscores with hyphens for valid slug
|
|
slug_suffix = level.value.replace("_", "-")
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug=f"project-{slug_suffix}",
|
|
autonomy_level=level,
|
|
)
|
|
assert project.autonomy_level == level
|
|
|
|
def test_invalid_autonomy_level(self):
|
|
"""Test that invalid autonomy level raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="invalid-autonomy",
|
|
autonomy_level="invalid", # type: ignore
|
|
)
|
|
|
|
def test_valid_project_statuses(self):
|
|
"""Test all valid project statuses."""
|
|
for status in ProjectStatus:
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug=f"project-status-{status.value}",
|
|
status=status,
|
|
)
|
|
assert project.status == status
|
|
|
|
def test_invalid_project_status(self):
|
|
"""Test that invalid project status raises ValidationError."""
|
|
with pytest.raises(ValidationError):
|
|
ProjectCreate(
|
|
name="Test Project",
|
|
slug="invalid-status",
|
|
status="invalid", # type: ignore
|
|
)
|
|
|
|
|
|
class TestProjectSettings:
|
|
"""Tests for Project settings validation."""
|
|
|
|
def test_settings_empty_dict(self):
|
|
"""Test that empty settings dict is valid."""
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug="empty-settings",
|
|
settings={},
|
|
)
|
|
assert project.settings == {}
|
|
|
|
def test_settings_complex_structure(self):
|
|
"""Test that complex settings structure is valid."""
|
|
complex_settings = {
|
|
"mcp_servers": ["gitea", "slack"],
|
|
"webhooks": {
|
|
"on_issue_created": "https://example.com",
|
|
},
|
|
"flags": True,
|
|
"count": 42,
|
|
}
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug="complex-settings",
|
|
settings=complex_settings,
|
|
)
|
|
assert project.settings == complex_settings
|
|
|
|
def test_settings_default_to_empty_dict(self):
|
|
"""Test that settings default to empty dict when not provided."""
|
|
project = ProjectCreate(
|
|
name="Test Project",
|
|
slug="default-settings",
|
|
)
|
|
assert project.settings == {}
|