Skip to content

Commit 3ced2a8

Browse files
authored
Merge pull request #6 from flowdacity/copilot/sub-pr-5
Address code review feedback: fix test assertions and exception handling
2 parents 523d2eb + cac9d56 commit 3ced2a8

1 file changed

Lines changed: 53 additions & 32 deletions

File tree

tests/test_routes.py

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
# tests/test_routes.py
22

33
# -*- coding: utf-8 -*-
4-
# Copyright ...
4+
# Copyright (c) 2025 Flowdacity Development Team. See LICENSE.txt for details.
55

66
import os
77
import unittest
88
import asyncio
99
import ujson as json
10-
from unittest.mock import AsyncMock, MagicMock, patch
10+
from unittest.mock import AsyncMock, patch
1111
from httpx import AsyncClient, ASGITransport
1212
from starlette.types import ASGIApp
13-
from redis.exceptions import LockError
1413
from fq_server import setup_server
1514
from fq_server.server import FQServer
1615

@@ -297,16 +296,17 @@ async def test_enqueue_get_queue_length_exception(self):
297296
"interval": 1000,
298297
}
299298

299+
# Mock get_queue_length to fail, but let enqueue succeed normally
300300
with patch.object(self.queue, "get_queue_length", side_effect=Exception("Redis error")):
301301
response = await self.client.post(
302302
"/enqueue/sms/test_queue_3/",
303303
content=json.dumps(request_params),
304304
headers={"Content-Type": "application/json"},
305305
)
306-
# Even if get_queue_length fails, enqueue proceeds (prints error)
307-
# The exception is caught and printed; enqueue still attempts
308-
# Check if response indicates the error
309-
self.assertIn(response.status_code, [201, 400])
306+
# When get_queue_length fails, enqueue still succeeds with current_queue_length=0
307+
self.assertEqual(response.status_code, 201)
308+
self.assertEqual(response.json()["status"], "queued")
309+
self.assertEqual(response.json()["current_queue_length"], 0)
310310

311311
async def test_enqueue_queue_enqueue_exception(self):
312312
"""Test enqueue when queue.enqueue() raises an exception."""
@@ -416,14 +416,15 @@ async def test_metrics_with_queue_type_exception(self):
416416
self.assertEqual(response.status_code, 400)
417417

418418
async def test_clear_queue_malformed_json(self):
419-
"""Test clear_queue - testing through the server's request body parsing."""
420-
# Note: httpx doesn't easily let us send raw body with DELETE,
421-
# so we test the exception path via mocking instead
422-
with patch.object(self.queue, "clear_queue", side_effect=Exception("Clear error")):
423-
# The server will still try to parse a body even if empty
424-
response = await self.client.delete("/deletequeue/sms/johndoe/")
425-
self.assertEqual(response.status_code, 400)
426-
self.assertEqual(response.json()["status"], "failure")
419+
"""Test clear_queue with malformed JSON body."""
420+
response = await self.client.request(
421+
"DELETE",
422+
"/deletequeue/sms/johndoe/",
423+
content=b"invalid json",
424+
headers={"Content-Type": "application/json"},
425+
)
426+
self.assertEqual(response.status_code, 400)
427+
self.assertEqual(response.json()["status"], "failure")
427428

428429
async def test_clear_queue_exception(self):
429430
"""Test clear_queue when queue.clear_queue() raises an exception."""
@@ -455,7 +456,7 @@ async def test_deep_status_exception(self):
455456
"""Test deep_status when queue.deep_status() raises an exception."""
456457
with patch.object(self.queue, "deep_status", side_effect=Exception("Status check failed")):
457458
with self.assertRaises(Exception):
458-
response = await self.client.get("/deepstatus/")
459+
await self.client.get("/deepstatus/")
459460

460461
async def test_deep_status_success(self):
461462
"""Test deep_status successful response."""
@@ -499,21 +500,35 @@ async def test_requeue_with_lock_disabled(self):
499500

500501
async def test_requeue_with_lock_lock_error(self):
501502
"""Test requeue_with_lock when lock acquisition fails with LockError."""
503+
from redis.exceptions import LockError
502504
server = self.server
503505

504-
# Use a real redis lock that times out
505-
requeue_task = asyncio.create_task(server.requeue_with_lock())
506-
507-
# Let it try to acquire lock and timeout (the default behavior when another process holds it)
508-
await asyncio.sleep(0.15)
506+
# Create an async context manager that raises LockError on enter
507+
class FailingLock:
508+
async def __aenter__(self):
509+
raise LockError("Failed to acquire lock")
510+
511+
async def __aexit__(self, *args):
512+
pass
509513

510-
# Cancel it
511-
requeue_task.cancel()
514+
# Mock redis_client with a lock method that returns the failing lock
515+
mock_redis = AsyncMock()
516+
# Make lock a regular (non-async) function that returns the context manager
517+
mock_redis.lock = lambda *args, **kwargs: FailingLock()
512518

513-
try:
514-
await requeue_task
515-
except asyncio.CancelledError:
516-
pass # Expected behavior
519+
with patch.object(server.queue, "redis_client", return_value=mock_redis):
520+
requeue_task = asyncio.create_task(server.requeue_with_lock())
521+
522+
# Let it try to acquire lock and handle LockError (sleeps and continues)
523+
await asyncio.sleep(0.15)
524+
525+
# Cancel it
526+
requeue_task.cancel()
527+
528+
try:
529+
await requeue_task
530+
except asyncio.CancelledError:
531+
pass # Expected - loop continues after LockError, then cancelled
517532

518533
async def test_requeue_with_lock_inner_exception(self):
519534
"""Test requeue_with_lock when requeue() inside lock context fails."""
@@ -563,8 +578,9 @@ async def test_lifespan_startup_shutdown(self):
563578
# Exit lifespan (shutdown)
564579
try:
565580
await lifespan_cm.__aexit__(None, None, None)
566-
except Exception:
567-
pass # May raise if task is cancelled
581+
except asyncio.CancelledError:
582+
# Expected if the requeue task is cancelled during shutdown
583+
pass
568584

569585
# Task should be cancelled or done
570586
await asyncio.sleep(0.05)
@@ -575,17 +591,22 @@ async def test_lifespaninitializes_queue(self):
575591
config_path = os.path.join(os.path.dirname(__file__), "test.conf")
576592
server = setup_server(config_path)
577593

578-
with patch.object(server.queue, "initialize", new_callable=AsyncMock) as mock_init:
594+
# Stub out both queue.initialize and the background requeue task to make
595+
# startup/shutdown deterministic and avoid hitting an uninitialized queue.
596+
with patch.object(server.queue, "initialize", new_callable=AsyncMock) as mock_init, \
597+
patch.object(server, "requeue_with_lock", new_callable=AsyncMock):
579598
lifespan_cm = server._lifespan(server.app)
580599
await lifespan_cm.__aenter__()
581600

582601
mock_init.assert_called_once()
583602

584603
# Cleanup
585-
server._requeue_task.cancel()
604+
if server._requeue_task is not None and not server._requeue_task.done():
605+
server._requeue_task.cancel()
586606
try:
587607
await lifespan_cm.__aexit__(None, None, None)
588-
except:
608+
except asyncio.CancelledError:
609+
# Expected if the requeue task is cancelled during shutdown
589610
pass
590611

591612

0 commit comments

Comments
 (0)