Add unit tests for dependency exception handling

- Created test_dependency_exception_handling.py with 5 comprehensive tests
- Tests verify proper handling of HTTPException in async generator dependencies
- All tests pass, confirming fix for 'generator didn't stop after athrow()' error
- Updated instructions with complete task documentation
This commit is contained in:
2026-01-19 19:44:48 +01:00
parent c97da7db2e
commit 62bdcf35cb
2 changed files with 180 additions and 13 deletions

View File

@@ -119,25 +119,40 @@ For each task completed:
## TODO List:
Task completed! ✅
**Task Completed Successfully**
### Issue Fixed
### Issue Fixed: RuntimeError: generator didn't stop after athrow()
Fixed the `RuntimeError: generator didn't stop after athrow()` error in the `/api/anime/add` endpoint.
**Problem:**
The `/api/anime/add` endpoint was throwing a 500 error with `RuntimeError: generator didn't stop after athrow()` when validation errors (HTTPException) were raised after database session dependencies yielded.
**Root Cause:**
The error occurred when an `HTTPException` was raised in an endpoint after a database session was yielded from the `get_optional_database_session` dependency. The async generator didn't properly handle exceptions thrown back into it, causing Python's async context manager to fail with "generator didn't stop after athrow()".
Async generator dependencies (`get_database_session` and `get_optional_database_session`) didn't properly handle exceptions thrown back into them after yielding. When an `HTTPException` was raised in the endpoint for validation errors, Python's async context manager tried to propagate the exception to the generator, but without proper exception handling around the yield statement, it resulted in the "generator didn't stop after athrow()" error.
**Solution:**
Added proper exception handling in both `get_database_session` and `get_optional_database_session` dependency functions by wrapping the yield statement in a try-except block that re-raises any exceptions, allowing FastAPI to handle them correctly.
Added proper exception handling in both database session dependencies by wrapping the `yield` statement with a try-except block that re-raises any exceptions, allowing FastAPI to handle them correctly.
**Changes Made:**
1. Fixed [src/server/utils/dependencies.py](src/server/utils/dependencies.py):
- Added try-except around yield in `get_database_session`
- Added try-except around yield in `get_optional_database_session`
2. Fixed [tests/api/test_anime_endpoints.py](tests/api/test_anime_endpoints.py):
- Added mock for `BackgroundLoaderService` to prevent initialization errors
- Updated test expectations to match 202 Accepted response status
**Files Modified:**
1. [src/server/utils/dependencies.py](src/server/utils/dependencies.py) - Fixed exception handling in `get_database_session` and `get_optional_database_session`
2. [tests/unit/test_dependency_exception_handling.py](tests/unit/test_dependency_exception_handling.py) - Created comprehensive unit tests for the fix
3. [tests/api/test_anime_endpoints.py](tests/api/test_anime_endpoints.py) - Updated to mock BackgroundLoaderService and expect 202 status codes
**Tests:**
All 16 anime endpoint tests now pass successfully, including validation scenarios that raise HTTPExceptions.
- ✅ 5 new unit tests for dependency exception handling (all passing)
- ✅ 16 anime endpoint integration tests (all passing)
- ✅ Tests verify proper handling of 400, 404, 422 status codes
- ✅ Tests verify successful requests still work correctly
**Note for Users:**
If you're experiencing this error on a running server, please **restart the server** to load the fixed code:
```bash
# Stop the server (Ctrl+C)
# Then restart:
conda run -n AniWorld python -m uvicorn src.server.fastapi_app:app --host 127.0.0.1 --port 8000 --reload
```
---
## Next Tasks:
No pending tasks at this time.

View File

@@ -0,0 +1,152 @@
"""
Unit tests for dependency exception handling in FastAPI dependencies.
This module tests that async generator dependencies properly handle exceptions
thrown back into them, preventing the "generator didn't stop after athrow()" error.
"""
import pytest
from fastapi import FastAPI, HTTPException, Depends
from httpx import AsyncClient, ASGITransport
from typing import AsyncGenerator, Optional
@pytest.mark.asyncio
async def test_get_optional_database_session_handles_http_exception():
"""Test that get_optional_database_session properly handles HTTPException.
This test verifies the fix for the "generator didn't stop after athrow()" error
that occurred when an HTTPException was raised after yielding a database session.
"""
from src.server.utils.dependencies import get_optional_database_session
# Create a test app
app = FastAPI()
@app.post("/test")
async def test_endpoint(
db: Optional[object] = Depends(get_optional_database_session)
):
"""Test endpoint that raises HTTPException after dependency yields."""
# Simulate validation error that raises HTTPException
raise HTTPException(status_code=400, detail="Validation error")
# Test the endpoint
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/test")
# Should return 400, not 500 (internal server error)
assert response.status_code == 400
assert response.json()["detail"] == "Validation error"
@pytest.mark.asyncio
async def test_get_database_session_handles_http_exception():
"""Test that get_database_session properly handles HTTPException.
This test verifies the fix for the "generator didn't stop after athrow()" error
that occurred when an HTTPException was raised after yielding a database session.
"""
from src.server.utils.dependencies import get_database_session
# Create a test app
app = FastAPI()
@app.post("/test")
async def test_endpoint(db: object = Depends(get_database_session)):
"""Test endpoint that raises HTTPException after dependency yields."""
# Simulate validation error that raises HTTPException
raise HTTPException(status_code=400, detail="Validation error")
# Test the endpoint - may get 501/503 if DB not available, or 400 if it is
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/test")
# Should return proper HTTP error, not 500 with generator error
assert response.status_code in (400, 501, 503)
assert "generator didn't stop" not in str(response.json())
@pytest.mark.asyncio
async def test_multiple_exceptions_in_optional_session():
"""Test that multiple different exceptions are handled correctly."""
from src.server.utils.dependencies import get_optional_database_session
app = FastAPI()
@app.post("/test-400")
async def test_400(db: Optional[object] = Depends(get_optional_database_session)):
raise HTTPException(status_code=400, detail="Bad request")
@app.post("/test-404")
async def test_404(db: Optional[object] = Depends(get_optional_database_session)):
raise HTTPException(status_code=404, detail="Not found")
@app.post("/test-422")
async def test_422(db: Optional[object] = Depends(get_optional_database_session)):
raise HTTPException(status_code=422, detail="Validation error")
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
# Test 400
response = await client.post("/test-400")
assert response.status_code == 400
# Test 404
response = await client.post("/test-404")
assert response.status_code == 404
# Test 422
response = await client.post("/test-422")
assert response.status_code == 422
@pytest.mark.asyncio
async def test_successful_request_with_optional_session():
"""Test that successful requests still work properly."""
from src.server.utils.dependencies import get_optional_database_session
app = FastAPI()
@app.post("/test")
async def test_endpoint(db: Optional[object] = Depends(get_optional_database_session)):
"""Test endpoint that succeeds."""
return {"status": "success", "db_available": db is not None}
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/test")
# Should return 200 for successful request
assert response.status_code == 200
data = response.json()
assert data["status"] == "success"
assert isinstance(data["db_available"], bool)
@pytest.mark.asyncio
async def test_exception_after_using_session():
"""Test exception raised after actually using the database session."""
from src.server.utils.dependencies import get_optional_database_session
app = FastAPI()
@app.post("/test")
async def test_endpoint(db: Optional[object] = Depends(get_optional_database_session)):
"""Test endpoint that uses db then raises exception."""
# Simulate using the database
if db is not None:
# In real code, would do: await db.execute(...)
pass
# Then raise an exception
raise HTTPException(status_code=400, detail="After using db")
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/test")
# Should properly handle the exception
assert response.status_code == 400
assert response.json()["detail"] == "After using db"