Files
fast-next-template/backend/tests/schemas/syndarix/test_issue_schemas.py
Felipe Cardoso 520a4d60fb 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>
2025-12-30 02:07:27 +01:00

343 lines
11 KiB
Python

# tests/schemas/syndarix/test_issue_schemas.py
"""
Tests for Issue schema validation.
"""
import uuid
import pytest
from pydantic import ValidationError
from app.schemas.syndarix import (
IssueAssign,
IssueCreate,
IssuePriority,
IssueStatus,
IssueUpdate,
SyncStatus,
)
class TestIssueCreateValidation:
"""Tests for IssueCreate schema validation."""
def test_valid_issue_create(self, valid_issue_data):
"""Test creating issue with valid data."""
issue = IssueCreate(**valid_issue_data)
assert issue.title == "Test Issue"
assert issue.body == "Issue description"
def test_issue_create_defaults(self, valid_issue_data):
"""Test that defaults are applied correctly."""
issue = IssueCreate(**valid_issue_data)
assert issue.status == IssueStatus.OPEN
assert issue.priority == IssuePriority.MEDIUM
assert issue.labels == []
assert issue.story_points is None
assert issue.assigned_agent_id is None
assert issue.human_assignee is None
assert issue.sprint_id is None
def test_issue_create_with_all_fields(self, valid_uuid):
"""Test creating issue with all optional fields."""
agent_id = uuid.uuid4()
sprint_id = uuid.uuid4()
issue = IssueCreate(
project_id=valid_uuid,
title="Full Issue",
body="Detailed body",
status=IssueStatus.IN_PROGRESS,
priority=IssuePriority.HIGH,
labels=["bug", "security"],
story_points=5,
assigned_agent_id=agent_id,
sprint_id=sprint_id,
external_tracker="gitea",
external_id="gitea-123",
external_url="https://gitea.example.com/issues/123",
external_number=123,
)
assert issue.status == IssueStatus.IN_PROGRESS
assert issue.priority == IssuePriority.HIGH
assert issue.labels == ["bug", "security"]
assert issue.story_points == 5
assert issue.external_tracker == "gitea"
def test_issue_create_title_empty_fails(self, valid_uuid):
"""Test that empty title raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueCreate(
project_id=valid_uuid,
title="",
)
errors = exc_info.value.errors()
assert any("title" in str(e) for e in errors)
def test_issue_create_title_whitespace_only_fails(self, valid_uuid):
"""Test that whitespace-only title raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueCreate(
project_id=valid_uuid,
title=" ",
)
errors = exc_info.value.errors()
assert any("title" in str(e) for e in errors)
def test_issue_create_title_stripped(self, valid_uuid):
"""Test that title is stripped."""
issue = IssueCreate(
project_id=valid_uuid,
title=" Padded Title ",
)
assert issue.title == "Padded Title"
def test_issue_create_project_id_required(self):
"""Test that project_id is required."""
with pytest.raises(ValidationError) as exc_info:
IssueCreate(title="No Project Issue")
errors = exc_info.value.errors()
assert any("project_id" in str(e).lower() for e in errors)
class TestIssueLabelsValidation:
"""Tests for Issue labels validation."""
def test_labels_normalized_lowercase(self, valid_uuid):
"""Test that labels are normalized to lowercase."""
issue = IssueCreate(
project_id=valid_uuid,
title="Test Issue",
labels=["Bug", "SECURITY", "FrontEnd"],
)
assert issue.labels == ["bug", "security", "frontend"]
def test_labels_stripped(self, valid_uuid):
"""Test that labels are stripped."""
issue = IssueCreate(
project_id=valid_uuid,
title="Test Issue",
labels=[" bug ", " security "],
)
assert issue.labels == ["bug", "security"]
def test_labels_empty_strings_removed(self, valid_uuid):
"""Test that empty label strings are removed."""
issue = IssueCreate(
project_id=valid_uuid,
title="Test Issue",
labels=["bug", "", " ", "security"],
)
assert issue.labels == ["bug", "security"]
class TestIssueStoryPointsValidation:
"""Tests for Issue story_points validation."""
def test_story_points_valid_range(self, valid_uuid):
"""Test valid story_points values."""
for points in [0, 1, 5, 13, 21, 100]:
issue = IssueCreate(
project_id=valid_uuid,
title="Test Issue",
story_points=points,
)
assert issue.story_points == points
def test_story_points_negative_fails(self, valid_uuid):
"""Test that negative story_points raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueCreate(
project_id=valid_uuid,
title="Test Issue",
story_points=-1,
)
errors = exc_info.value.errors()
assert any("story_points" in str(e).lower() for e in errors)
def test_story_points_over_100_fails(self, valid_uuid):
"""Test that story_points > 100 raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueCreate(
project_id=valid_uuid,
title="Test Issue",
story_points=101,
)
errors = exc_info.value.errors()
assert any("story_points" in str(e).lower() for e in errors)
class TestIssueExternalTrackerValidation:
"""Tests for Issue external tracker validation."""
def test_valid_external_trackers(self, valid_uuid):
"""Test valid external tracker values."""
for tracker in ["gitea", "github", "gitlab"]:
issue = IssueCreate(
project_id=valid_uuid,
title="Test Issue",
external_tracker=tracker,
external_id="ext-123",
)
assert issue.external_tracker == tracker
def test_invalid_external_tracker(self, valid_uuid):
"""Test that invalid external tracker raises ValidationError."""
with pytest.raises(ValidationError):
IssueCreate(
project_id=valid_uuid,
title="Test Issue",
external_tracker="invalid", # type: ignore
external_id="ext-123",
)
class TestIssueUpdateValidation:
"""Tests for IssueUpdate schema validation."""
def test_issue_update_partial(self):
"""Test updating only some fields."""
update = IssueUpdate(
title="Updated Title",
)
assert update.title == "Updated Title"
assert update.body is None
assert update.status is None
def test_issue_update_all_fields(self):
"""Test updating all fields."""
agent_id = uuid.uuid4()
sprint_id = uuid.uuid4()
update = IssueUpdate(
title="Updated Title",
body="Updated body",
status=IssueStatus.CLOSED,
priority=IssuePriority.CRITICAL,
labels=["updated"],
assigned_agent_id=agent_id,
human_assignee=None,
sprint_id=sprint_id,
story_points=8,
sync_status=SyncStatus.PENDING,
)
assert update.title == "Updated Title"
assert update.status == IssueStatus.CLOSED
assert update.priority == IssuePriority.CRITICAL
def test_issue_update_empty_title_fails(self):
"""Test that empty title in update raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueUpdate(title="")
errors = exc_info.value.errors()
assert any("title" in str(e) for e in errors)
def test_issue_update_labels_normalized(self):
"""Test that labels are normalized in updates."""
update = IssueUpdate(
labels=["Bug", "SECURITY"],
)
assert update.labels == ["bug", "security"]
class TestIssueAssignValidation:
"""Tests for IssueAssign schema validation."""
def test_assign_to_agent(self):
"""Test assigning to an agent."""
agent_id = uuid.uuid4()
assign = IssueAssign(assigned_agent_id=agent_id)
assert assign.assigned_agent_id == agent_id
assert assign.human_assignee is None
def test_assign_to_human(self):
"""Test assigning to a human."""
assign = IssueAssign(human_assignee="developer@example.com")
assert assign.human_assignee == "developer@example.com"
assert assign.assigned_agent_id is None
def test_unassign(self):
"""Test unassigning (both None)."""
assign = IssueAssign()
assert assign.assigned_agent_id is None
assert assign.human_assignee is None
def test_assign_both_fails(self):
"""Test that assigning to both agent and human raises ValidationError."""
with pytest.raises(ValidationError) as exc_info:
IssueAssign(
assigned_agent_id=uuid.uuid4(),
human_assignee="developer@example.com",
)
errors = exc_info.value.errors()
# Check for the validation error message
assert len(errors) > 0
class TestIssueEnums:
"""Tests for Issue enum validation."""
def test_valid_issue_statuses(self, valid_uuid):
"""Test all valid issue statuses."""
for status in IssueStatus:
issue = IssueCreate(
project_id=valid_uuid,
title=f"Issue {status.value}",
status=status,
)
assert issue.status == status
def test_invalid_issue_status(self, valid_uuid):
"""Test that invalid issue status raises ValidationError."""
with pytest.raises(ValidationError):
IssueCreate(
project_id=valid_uuid,
title="Test Issue",
status="invalid", # type: ignore
)
def test_valid_issue_priorities(self, valid_uuid):
"""Test all valid issue priorities."""
for priority in IssuePriority:
issue = IssueCreate(
project_id=valid_uuid,
title=f"Issue {priority.value}",
priority=priority,
)
assert issue.priority == priority
def test_invalid_issue_priority(self, valid_uuid):
"""Test that invalid issue priority raises ValidationError."""
with pytest.raises(ValidationError):
IssueCreate(
project_id=valid_uuid,
title="Test Issue",
priority="invalid", # type: ignore
)
def test_valid_sync_statuses(self):
"""Test all valid sync statuses in update."""
for status in SyncStatus:
update = IssueUpdate(sync_status=status)
assert update.sync_status == status