feat(backend): Add Syndarix domain models with CRUD operations
- 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>
This commit is contained in:
300
backend/tests/schemas/syndarix/test_project_schemas.py
Normal file
300
backend/tests/schemas/syndarix/test_project_schemas.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# 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 == {}
|
||||
Reference in New Issue
Block a user