test_projects_stream.py 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. import json
  2. from unittest.mock import AsyncMock, patch
  3. import pytest
  4. from httpx import AsyncClient
  5. # Adjust this import to match where you instantiated your broadcast object!
  6. from app.main import app
  7. from app.models import OrgRole, Project
  8. from app.tests.conftest import default_project_id
  9. pytestmark = pytest.mark.asyncio
  10. class TestProjectStream:
  11. @pytest.mark.parametrize(
  12. "role, expected_status",
  13. [
  14. (OrgRole.ORG_ADMIN, 200),
  15. (OrgRole.RESPO_BENEVOLE, 200),
  16. (OrgRole.RESPO_COMMISSION, 200),
  17. (None, 403),
  18. ],
  19. )
  20. async def test_role_access(
  21. self, client: AsyncClient, default_project: Project, make_org_user, role, expected_status
  22. ):
  23. """Test that only authorized roles can connect to the stream."""
  24. _, headers = make_org_user(role=role)
  25. url = app.url_path_for("project_stream", project_id=default_project_id)
  26. # We mock 'broadcast.subscribe' so it immediately returns an empty async iterator
  27. # This prevents the endpoint from hanging forever in the test
  28. with patch("app.core.events.broadcast.subscribe") as mock_subscribe:
  29. # Create an async generator that yields nothing and closes
  30. async def mock_generator():
  31. return
  32. yield
  33. mock_subscriber = AsyncMock()
  34. mock_subscriber.__aenter__.return_value = mock_generator()
  35. mock_subscribe.return_value = mock_subscriber
  36. async with client.stream(
  37. "GET", f"{url}?client_id=test-auth", headers=headers
  38. ) as response:
  39. assert response.status_code == expected_status
  40. async def test_receives_sse_events(
  41. self, client: AsyncClient, default_project: Project, make_org_user
  42. ):
  43. """Test that events from the broadcaster are correctly formatted as SSE."""
  44. _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
  45. url = app.url_path_for("project_stream", project_id=default_project_id)
  46. # Create a fake Redis event
  47. class FakeEvent:
  48. def __init__(self):
  49. self.message = json.dumps(
  50. {
  51. "client_id": "someone-else",
  52. "event": "slot_updated",
  53. "item": {"id": "123", "title": "Installation"},
  54. }
  55. )
  56. with patch("app.core.events.broadcast.subscribe") as mock_subscribe:
  57. # Make the mocked subscribe yield our FakeEvent, then stop
  58. async def mock_generator():
  59. yield FakeEvent()
  60. mock_subscriber = AsyncMock()
  61. mock_subscriber.__aenter__.return_value = mock_generator()
  62. mock_subscribe.return_value = mock_subscriber
  63. async with client.stream(
  64. "GET", f"{url}?client_id=listener-client", headers=headers
  65. ) as response:
  66. assert response.status_code == 200
  67. # Read the response
  68. lines = [line async for line in response.aiter_lines() if line.strip()]
  69. # Verify the formatting is standard SSE format
  70. assert lines[0] == "event: slot_updated"
  71. assert "Installation" in lines[1]