Skip to content

Commit d9f86f9

Browse files
authored
feat: fixed fixtures in backend/e2e (#237)
* feat: fixed fixtures in backend/e2e * feat: extract default exec request + removed redis client dep in sse routes tests * fix: simple exec script as a fixture
1 parent cb3f2e3 commit d9f86f9

5 files changed

Lines changed: 108 additions & 116 deletions

File tree

backend/tests/e2e/conftest.py

Lines changed: 31 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from collections.abc import Callable
44

55
import pytest
6-
import pytest_asyncio
76
import redis.asyncio as redis
87
from app.db.docs.saga import SagaDocument
98
from app.domain.enums import EventType, UserRole
@@ -94,7 +93,7 @@ async def wait_for_notification(
9493

9594

9695
@pytest.fixture
97-
def simple_execution_request() -> ExecutionRequest:
96+
def exec_request() -> ExecutionRequest:
9897
"""Simple python print execution."""
9998
return ExecutionRequest(script="print('test')", lang="python", lang_version="3.11")
10099

@@ -155,74 +154,60 @@ def new_script_request() -> SavedScriptCreateRequest:
155154
)
156155

157156

158-
@pytest_asyncio.fixture
159-
async def created_execution(
160-
test_user: AsyncClient, simple_execution_request: ExecutionRequest
157+
async def create_execution(
158+
client: AsyncClient,
159+
request: ExecutionRequest,
161160
) -> ExecutionResponse:
162-
"""Execution created by test_user (does NOT wait for completion)."""
163-
resp = await test_user.post(
164-
"/api/v1/execute", json=simple_execution_request.model_dump()
165-
)
161+
"""POST /execute and return response (does NOT wait for completion)."""
162+
resp = await client.post("/api/v1/execute", json=request.model_dump())
166163
assert resp.status_code == 200
167164
return ExecutionResponse.model_validate(resp.json())
168165

169166

170-
@pytest_asyncio.fixture
171-
async def created_execution_admin(
172-
test_admin: AsyncClient, simple_execution_request: ExecutionRequest
173-
) -> ExecutionResponse:
174-
"""Execution created by test_admin."""
175-
resp = await test_admin.post(
176-
"/api/v1/execute", json=simple_execution_request.model_dump()
177-
)
178-
assert resp.status_code == 200
179-
return ExecutionResponse.model_validate(resp.json())
180-
181-
182-
@pytest_asyncio.fixture
183-
async def execution_with_saga(
167+
async def create_execution_with_saga(
168+
client: AsyncClient,
184169
redis_client: redis.Redis,
185-
created_execution: ExecutionResponse,
170+
request: ExecutionRequest,
186171
) -> tuple[ExecutionResponse, SagaStatusResponse]:
187-
"""Execution with saga guaranteed in MongoDB (via POD_CREATED event).
172+
"""Create execution, wait for POD_CREATED, return (execution, saga).
188173
189-
POD_CREATED arrives after the saga orchestrator has persisted the saga
190-
document and dispatched the create-pod command. We query Beanie directly
191-
(same DB, no HTTP round-trip) for a deterministic, sleep-free lookup.
174+
POD_CREATED implies the saga orchestrator persisted the document and
175+
dispatched the create-pod command.
192176
"""
193-
await wait_for_pod_created(redis_client, created_execution.execution_id)
177+
execution = await create_execution(client, request)
178+
await wait_for_pod_created(redis_client, execution.execution_id)
194179

195-
doc = await SagaDocument.find_one(SagaDocument.execution_id == created_execution.execution_id)
180+
doc = await SagaDocument.find_one(SagaDocument.execution_id == execution.execution_id)
196181
assert doc is not None, (
197-
f"No saga document for {created_execution.execution_id} despite POD_CREATED received"
182+
f"No saga document for {execution.execution_id} despite POD_CREATED received"
198183
)
199184

200185
saga = SagaStatusResponse.model_validate(doc, from_attributes=True)
201-
assert saga.execution_id == created_execution.execution_id
202-
return created_execution, saga
186+
return execution, saga
203187

204188

205-
@pytest_asyncio.fixture
206-
async def execution_with_notification(
207-
test_user: AsyncClient,
189+
async def create_execution_with_notification(
190+
client: AsyncClient,
208191
redis_client: redis.Redis,
209-
created_execution: ExecutionResponse,
192+
request: ExecutionRequest,
193+
*,
194+
timeout: float = 30.0,
210195
) -> tuple[ExecutionResponse, NotificationResponse]:
211-
"""Execution with notification guaranteed in MongoDB.
196+
"""Create execution, wait for notification delivery, return (execution, notification).
212197
213-
Waits on sse:notif:{user_id} — the notification service publishes there
214-
only after persisting to MongoDB. Unlike RESULT_STORED (which comes from
215-
an independent consumer group), this is a correct readiness signal.
198+
Fetches user_id from /auth/me, waits on sse:notif:{user_id} (correct
199+
signal — notification service publishes after MongoDB persist), then
200+
queries the notification list.
216201
"""
217-
me_resp = await test_user.get("/api/v1/auth/me")
202+
me_resp = await client.get("/api/v1/auth/me")
218203
assert me_resp.status_code == 200
219204
user_id = me_resp.json()["user_id"]
220205

221-
await wait_for_notification(redis_client, user_id)
206+
execution = await create_execution(client, request)
207+
await wait_for_notification(redis_client, user_id, timeout=timeout)
222208

223-
resp = await test_user.get("/api/v1/notifications", params={"limit": 10})
209+
resp = await client.get("/api/v1/notifications", params={"limit": 10})
224210
assert resp.status_code == 200
225211
result = NotificationListResponse.model_validate(resp.json())
226212
assert result.notifications, "No notification after SSE delivery"
227-
notification = result.notifications[0]
228-
return created_execution, notification
213+
return execution, result.notifications[0]

backend/tests/e2e/test_execution_routes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ class TestExecutionHappyPath:
8383

8484
@pytest.mark.asyncio
8585
async def test_execute_simple_script_completes(
86-
self, test_user: AsyncClient, redis_client: redis.Redis, simple_execution_request: ExecutionRequest
86+
self, test_user: AsyncClient, redis_client: redis.Redis, exec_request: ExecutionRequest
8787
) -> None:
8888
"""Simple script executes and completes successfully."""
89-
exec_response, result = await submit_and_wait(test_user, redis_client, simple_execution_request)
89+
exec_response, result = await submit_and_wait(test_user, redis_client, exec_request)
9090

9191
assert exec_response.execution_id
9292
assert result.status == ExecutionStatus.COMPLETED

backend/tests/e2e/test_notifications_routes.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import pytest
2+
import redis.asyncio as redis
23
from app.domain.enums import NotificationChannel, NotificationSeverity, NotificationStatus
3-
from app.schemas_pydantic.execution import ExecutionResponse
4+
from app.schemas_pydantic.execution import ExecutionRequest
45
from app.schemas_pydantic.notification import (
56
DeleteNotificationResponse,
67
NotificationListResponse,
7-
NotificationResponse,
88
NotificationSubscription,
99
SubscriptionsResponse,
1010
UnreadCountResponse,
1111
)
1212
from httpx import AsyncClient
1313

14+
from tests.e2e.conftest import create_execution_with_notification
15+
1416
pytestmark = [pytest.mark.e2e, pytest.mark.kafka]
1517

1618

@@ -104,10 +106,11 @@ async def test_mark_nonexistent_notification_read(
104106
async def test_mark_notification_read(
105107
self,
106108
test_user: AsyncClient,
107-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
109+
redis_client: redis.Redis,
110+
exec_request: ExecutionRequest,
108111
) -> None:
109112
"""Mark existing notification as read."""
110-
_, notification = execution_with_notification
113+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
111114

112115
response = await test_user.put(
113116
f"/api/v1/notifications/{notification.notification_id}/read"
@@ -131,19 +134,19 @@ class TestMarkAllRead:
131134
async def test_mark_all_read(
132135
self,
133136
test_user: AsyncClient,
134-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
137+
redis_client: redis.Redis,
138+
exec_request: ExecutionRequest,
135139
) -> None:
136140
"""Mark all notifications as read returns 204."""
137-
_, notification = execution_with_notification
141+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
138142

139143
response = await test_user.post("/api/v1/notifications/mark-all-read")
140144
assert response.status_code == 204
141145

142146
# Verify the specific notification was marked as read.
143147
# We assert on the individual notification rather than global unread_count
144-
# because other tests create executions (via created_execution without
145-
# waiting for notifications) whose async notifications can arrive between
146-
# mark-all-read and any subsequent count query.
148+
# because other tests create executions whose async notifications can
149+
# arrive between mark-all-read and any subsequent count query.
147150
resp = await test_user.get("/api/v1/notifications")
148151
assert resp.status_code == 200
149152
result = NotificationListResponse.model_validate(resp.json())
@@ -284,9 +287,12 @@ class TestUnreadCount:
284287
async def test_get_unread_count(
285288
self,
286289
test_user: AsyncClient,
287-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
290+
redis_client: redis.Redis,
291+
exec_request: ExecutionRequest,
288292
) -> None:
289293
"""Get unread notification count."""
294+
await create_execution_with_notification(test_user, redis_client, exec_request)
295+
290296
response = await test_user.get("/api/v1/notifications/unread-count")
291297

292298
assert response.status_code == 200
@@ -313,10 +319,11 @@ async def test_delete_nonexistent_notification(
313319
async def test_delete_notification(
314320
self,
315321
test_user: AsyncClient,
316-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
322+
redis_client: redis.Redis,
323+
exec_request: ExecutionRequest,
317324
) -> None:
318325
"""Delete existing notification returns success."""
319-
_, notification = execution_with_notification
326+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
320327

321328
response = await test_user.delete(
322329
f"/api/v1/notifications/{notification.notification_id}"
@@ -342,10 +349,11 @@ async def test_user_cannot_see_other_users_notifications(
342349
self,
343350
test_user: AsyncClient,
344351
another_user: AsyncClient,
345-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
352+
redis_client: redis.Redis,
353+
exec_request: ExecutionRequest,
346354
) -> None:
347355
"""User's notification list does not include other users' notifications."""
348-
_, notification = execution_with_notification
356+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
349357

350358
response = await another_user.get("/api/v1/notifications")
351359
assert response.status_code == 200
@@ -360,10 +368,11 @@ async def test_cannot_mark_other_users_notification_read(
360368
self,
361369
test_user: AsyncClient,
362370
another_user: AsyncClient,
363-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
371+
redis_client: redis.Redis,
372+
exec_request: ExecutionRequest,
364373
) -> None:
365374
"""Cannot mark another user's notification as read."""
366-
_, notification = execution_with_notification
375+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
367376

368377
response = await another_user.put(
369378
f"/api/v1/notifications/{notification.notification_id}/read"
@@ -382,10 +391,11 @@ async def test_cannot_delete_other_users_notification(
382391
self,
383392
test_user: AsyncClient,
384393
another_user: AsyncClient,
385-
execution_with_notification: tuple[ExecutionResponse, NotificationResponse],
394+
redis_client: redis.Redis,
395+
exec_request: ExecutionRequest,
386396
) -> None:
387397
"""Cannot delete another user's notification."""
388-
_, notification = execution_with_notification
398+
_, notification = await create_execution_with_notification(test_user, redis_client, exec_request)
389399

390400
response = await another_user.delete(
391401
f"/api/v1/notifications/{notification.notification_id}"

0 commit comments

Comments
 (0)