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
66import os
77import unittest
88import asyncio
99import ujson as json
10- from unittest .mock import AsyncMock , MagicMock , patch
10+ from unittest .mock import AsyncMock , patch
1111from httpx import AsyncClient , ASGITransport
1212from starlette .types import ASGIApp
13- from redis .exceptions import LockError
1413from fq_server import setup_server
1514from 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