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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user