forked from cartsnitch/cartsnitch
feat(api): implement lifespan with DB and Redis connection pooling
- Refactor database.py to use init_db()/close_db() lifecycle - Add create_db_engine() with pool_size=10, max_overflow=20, pool_pre_ping=True - Replace cache.py stub with real Redis client using redis.asyncio - Implement init_redis()/close_redis() with graceful error handling - Replace no-op lifespan in main.py with proper startup/shutdown - Enhance health endpoint to check DB and Redis connectivity - Add tests for database, cache, and health endpoint lifecycle Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
committed by
savannah-savings-cto[bot]
parent
f96daceb0f
commit
2460a00d4e
@@ -0,0 +1,50 @@
|
||||
"""Tests for Redis/DragonflyDB caching lifecycle."""
|
||||
|
||||
import pytest
|
||||
|
||||
from cartsnitch_api.cache import CacheClient, close_redis, get_redis, init_redis
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_redis_creates_client():
|
||||
"""Test that init_redis creates the Redis client."""
|
||||
await init_redis()
|
||||
try:
|
||||
r = get_redis()
|
||||
assert r is not None
|
||||
await r.ping()
|
||||
finally:
|
||||
await close_redis()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_redis_clears_client():
|
||||
"""Test that close_redis properly closes and clears the client."""
|
||||
await init_redis()
|
||||
await close_redis()
|
||||
assert get_redis() is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_client_get_returns_none_when_not_connected():
|
||||
"""Test that CacheClient.get returns None gracefully when Redis is down."""
|
||||
client = CacheClient()
|
||||
# Without init_redis, get should return None
|
||||
result = await client.get("test-key")
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_client_set_does_not_raise_when_not_connected():
|
||||
"""Test that CacheClient.set does not raise when Redis is down."""
|
||||
client = CacheClient()
|
||||
# Without init_redis, set should not raise
|
||||
await client.set("test-key", "test-value", ttl_seconds=60)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cache_client_delete_does_not_raise_when_not_connected():
|
||||
"""Test that CacheClient.delete does not raise when Redis is down."""
|
||||
client = CacheClient()
|
||||
# Without init_redis, delete should not raise
|
||||
await client.delete("test-key")
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for database initialization and lifecycle."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from cartsnitch_api.database import (
|
||||
close_db,
|
||||
create_db_engine,
|
||||
get_engine,
|
||||
init_db,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_db_engine_creates_engine_with_pool_settings():
|
||||
"""Test that create_db_engine creates engine with correct pool settings."""
|
||||
engine = create_db_engine()
|
||||
assert engine is not None
|
||||
pool = engine.pool
|
||||
assert pool.size() == 10
|
||||
assert pool._max_overflow == 20
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_db_sets_engine_and_factory():
|
||||
"""Test that init_db properly initializes the engine and session factory."""
|
||||
await init_db()
|
||||
try:
|
||||
eng = get_engine()
|
||||
assert eng is not None
|
||||
from cartsnitch_api import database
|
||||
|
||||
assert database.async_session_factory is not None
|
||||
finally:
|
||||
await close_db()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_close_db_disposes_engine():
|
||||
"""Test that close_db properly disposes the engine."""
|
||||
await init_db()
|
||||
await close_db()
|
||||
assert get_engine() is None
|
||||
from cartsnitch_api import database
|
||||
|
||||
assert database.async_session_factory is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_db_yields_session_after_init():
|
||||
"""Test that get_db yields working sessions after init_db."""
|
||||
await init_db()
|
||||
try:
|
||||
from cartsnitch_api.database import get_db
|
||||
|
||||
gen = get_db()
|
||||
session = await gen.__anext__()
|
||||
assert isinstance(session, AsyncSession)
|
||||
await gen.aclose()
|
||||
finally:
|
||||
await close_db()
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Tests for health check endpoint."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from cartsnitch_api.database import init_db, close_db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_returns_db_and_redis_fields(client):
|
||||
"""Test that health endpoint returns db and redis status fields."""
|
||||
from cartsnitch_api.cache import init_redis, close_redis
|
||||
|
||||
await init_db()
|
||||
await init_redis()
|
||||
|
||||
try:
|
||||
response = await client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "status" in data
|
||||
assert "db" in data
|
||||
assert "redis" in data
|
||||
finally:
|
||||
await close_redis()
|
||||
await close_db()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_returns_degraded_when_db_down():
|
||||
"""Test that health returns degraded when database is down."""
|
||||
from cartsnitch_api.database import _engine
|
||||
from cartsnitch_api.routes.health import health
|
||||
|
||||
# Simulate engine is None (DB not initialized)
|
||||
with patch("cartsnitch_api.routes.health.get_engine", return_value=None):
|
||||
response = await health()
|
||||
assert response["status"] == "degraded"
|
||||
assert response["db"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_returns_ok_when_db_up(client):
|
||||
"""Test that health returns ok when database is up."""
|
||||
from cartsnitch_api.database import init_db, close_db
|
||||
from cartsnitch_api.cache import init_redis, close_redis
|
||||
|
||||
await init_db()
|
||||
await init_redis()
|
||||
|
||||
try:
|
||||
response = await client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
if data["db"]:
|
||||
assert data["status"] == "ok"
|
||||
finally:
|
||||
await close_redis()
|
||||
await close_db()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_redis_down_does_not_make_unhealthy(client):
|
||||
"""Test that Redis being down does not make health return unhealthy."""
|
||||
from cartsnitch_api.database import init_db, close_db
|
||||
|
||||
await init_db()
|
||||
|
||||
try:
|
||||
response = await client.get("/health")
|
||||
data = response.json()
|
||||
# Redis being down should not make status "degraded"
|
||||
# Only DB failure makes it degraded
|
||||
if not data["db"]:
|
||||
assert data["status"] == "degraded"
|
||||
finally:
|
||||
await close_db()
|
||||
Reference in New Issue
Block a user