fix: Add missing API endpoints and validation improvements

- Add cancel_sprint and delete_sprint endpoints to sprints.py
- Add unassign_issue endpoint to issues.py
- Add remove_issue_from_sprint endpoint to sprints.py
- Add CRUD methods: remove_sprint_from_issues, unassign, remove_from_sprint
- Add validation to prevent closed issues in active/planned sprints
- Add authorization tests for SSE events endpoint
- Fix IDOR vulnerabilities in agents.py and projects.py
- Add Syndarix models migration (0004)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 15:39:51 +01:00
parent b43fa8ace2
commit cea97afe25
10 changed files with 1156 additions and 74 deletions

View File

@@ -23,8 +23,10 @@ from httpx import ASGITransport, AsyncClient
from app.api.dependencies.event_bus import get_event_bus
from app.core.database import get_db
from app.crud.syndarix.project import project as project_crud
from app.main import app
from app.schemas.events import Event, EventType
from app.schemas.syndarix.project import ProjectCreate
from app.services.event_bus import EventBus
@@ -147,6 +149,21 @@ async def user_token_with_mock_bus(client_with_mock_bus, async_test_user):
return tokens["access_token"]
@pytest_asyncio.fixture
async def test_project_for_events(async_test_db, async_test_user):
"""Create a test project owned by the test user for events testing."""
_test_engine, AsyncTestingSessionLocal = async_test_db
async with AsyncTestingSessionLocal() as session:
project_in = ProjectCreate(
name="Test Events Project",
slug="test-events-project",
owner_id=async_test_user.id,
)
project = await project_crud.create(session, obj_in=project_in)
return project
class TestSSEEndpointAuthentication:
"""Tests for SSE endpoint authentication."""
@@ -174,15 +191,75 @@ class TestSSEEndpointAuthentication:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
class TestSSEEndpointAuthorization:
"""Tests for SSE endpoint authorization."""
@pytest.mark.asyncio
async def test_stream_events_nonexistent_project_returns_403(
self, client_with_mock_bus, user_token_with_mock_bus
):
"""Test that accessing a non-existent project returns 403."""
nonexistent_project_id = uuid.uuid4()
response = await client_with_mock_bus.get(
f"/api/v1/projects/{nonexistent_project_id}/events/stream",
headers={"Authorization": f"Bearer {user_token_with_mock_bus}"},
timeout=5.0,
)
# Should return 403 because project doesn't exist (auth check fails)
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.asyncio
async def test_stream_events_other_users_project_returns_403(
self, client_with_mock_bus, user_token_with_mock_bus, async_test_db
):
"""Test that accessing another user's project returns 403."""
_test_engine, AsyncTestingSessionLocal = async_test_db
# Create a project owned by a different user
async with AsyncTestingSessionLocal() as session:
other_user_id = uuid.uuid4() # Simulated other user
project_in = ProjectCreate(
name="Other User's Project",
slug="other-users-project",
owner_id=other_user_id,
)
other_project = await project_crud.create(session, obj_in=project_in)
response = await client_with_mock_bus.get(
f"/api/v1/projects/{other_project.id}/events/stream",
headers={"Authorization": f"Bearer {user_token_with_mock_bus}"},
timeout=5.0,
)
# Should return 403 because user doesn't own the project
assert response.status_code == status.HTTP_403_FORBIDDEN
@pytest.mark.asyncio
async def test_send_test_event_nonexistent_project_returns_403(
self, client_with_mock_bus, user_token_with_mock_bus
):
"""Test that sending event to non-existent project returns 403."""
nonexistent_project_id = uuid.uuid4()
response = await client_with_mock_bus.post(
f"/api/v1/projects/{nonexistent_project_id}/events/test",
headers={"Authorization": f"Bearer {user_token_with_mock_bus}"},
)
assert response.status_code == status.HTTP_403_FORBIDDEN
class TestSSEEndpointStream:
"""Tests for SSE stream functionality."""
@pytest.mark.asyncio
async def test_stream_events_returns_sse_response(
self, client_with_mock_bus, user_token_with_mock_bus
self, client_with_mock_bus, user_token_with_mock_bus, test_project_for_events
):
"""Test that SSE endpoint returns proper SSE response."""
project_id = uuid.uuid4()
project_id = test_project_for_events.id
# Make request with a timeout to avoid hanging
response = await client_with_mock_bus.get(
@@ -197,10 +274,10 @@ class TestSSEEndpointStream:
@pytest.mark.asyncio
async def test_stream_events_with_events(
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus, test_project_for_events
):
"""Test that SSE endpoint yields events."""
project_id = uuid.uuid4()
project_id = test_project_for_events.id
# Create a test event and add it to the mock bus
test_event = Event(
@@ -228,10 +305,10 @@ class TestSSEEndpointStream:
@pytest.mark.asyncio
async def test_stream_events_with_last_event_id(
self, client_with_mock_bus, user_token_with_mock_bus
self, client_with_mock_bus, user_token_with_mock_bus, test_project_for_events
):
"""Test that Last-Event-ID header is accepted."""
project_id = uuid.uuid4()
project_id = test_project_for_events.id
last_event_id = str(uuid.uuid4())
response = await client_with_mock_bus.get(
@@ -252,10 +329,10 @@ class TestSSEEndpointHeaders:
@pytest.mark.asyncio
async def test_stream_events_cache_control_header(
self, client_with_mock_bus, user_token_with_mock_bus
self, client_with_mock_bus, user_token_with_mock_bus, test_project_for_events
):
"""Test that SSE response has no-cache header."""
project_id = uuid.uuid4()
project_id = test_project_for_events.id
response = await client_with_mock_bus.get(
f"/api/v1/projects/{project_id}/events/stream",
@@ -284,10 +361,10 @@ class TestTestEventEndpoint:
@pytest.mark.asyncio
async def test_send_test_event_success(
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus
self, client_with_mock_bus, user_token_with_mock_bus, mock_event_bus, test_project_for_events
):
"""Test sending a test event."""
project_id = uuid.uuid4()
project_id = test_project_for_events.id
response = await client_with_mock_bus.post(
f"/api/v1/projects/{project_id}/events/test",