feat: implement Redis caching for analytics endpoints with fallback to database
Test / test (push) Successful in 15s
Test / test (push) Successful in 15s
This commit is contained in:
@@ -8,10 +8,11 @@ import pytest_asyncio
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||
|
||||
from app.api.deps import get_db_session
|
||||
from app.api.deps import get_cache_backend, get_db_session
|
||||
from app.core.security import password_hasher
|
||||
from app.main import create_app
|
||||
from app.models import Base
|
||||
from tests.utils.fake_redis import InMemoryRedis
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -41,6 +42,7 @@ async def session_factory() -> AsyncGenerator[async_sessionmaker[AsyncSession],
|
||||
@pytest_asyncio.fixture()
|
||||
async def client(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
cache_stub: InMemoryRedis,
|
||||
) -> AsyncGenerator[AsyncClient, None]:
|
||||
app = create_app()
|
||||
|
||||
@@ -54,6 +56,12 @@ async def client(
|
||||
raise
|
||||
|
||||
app.dependency_overrides[get_db_session] = _get_session_override
|
||||
app.dependency_overrides[get_cache_backend] = lambda: cache_stub
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://testserver") as test_client:
|
||||
yield test_client
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cache_stub() -> InMemoryRedis:
|
||||
return InMemoryRedis()
|
||||
|
||||
@@ -23,6 +23,7 @@ class AnalyticsScenario:
|
||||
user_id: int
|
||||
user_email: str
|
||||
token: str
|
||||
in_progress_deal_id: int
|
||||
|
||||
|
||||
async def prepare_analytics_scenario(session_factory: async_sessionmaker[AsyncSession]) -> AnalyticsScenario:
|
||||
@@ -102,6 +103,7 @@ async def prepare_analytics_scenario(session_factory: async_sessionmaker[AsyncSe
|
||||
user_id=user.id,
|
||||
user_email=user.email,
|
||||
token=token,
|
||||
in_progress_deal_id=next(deal.id for deal in deals if deal.status is DealStatus.IN_PROGRESS),
|
||||
)
|
||||
|
||||
|
||||
@@ -163,4 +165,37 @@ async def test_deals_funnel_returns_breakdown(
|
||||
qualification = next(item for item in payload["stages"] if item["stage"] == DealStage.QUALIFICATION.value)
|
||||
assert qualification["total"] == 1
|
||||
proposal = next(item for item in payload["stages"] if item["stage"] == DealStage.PROPOSAL.value)
|
||||
assert proposal["conversion_to_next"] == 100.0
|
||||
assert proposal["conversion_to_next"] == 100.0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deal_update_invalidates_cached_summary(
|
||||
session_factory: async_sessionmaker[AsyncSession],
|
||||
client: AsyncClient,
|
||||
cache_stub,
|
||||
) -> None:
|
||||
scenario = await prepare_analytics_scenario(session_factory)
|
||||
headers = _headers(scenario.token, scenario.organization_id)
|
||||
|
||||
first = await client.get(
|
||||
"/api/v1/analytics/deals/summary?days=30",
|
||||
headers=headers,
|
||||
)
|
||||
assert first.status_code == 200
|
||||
keys = [key async for key in cache_stub.scan_iter("analytics:summary:*")]
|
||||
assert keys, "cache should contain warmed summary"
|
||||
|
||||
patch_response = await client.patch(
|
||||
f"/api/v1/deals/{scenario.in_progress_deal_id}",
|
||||
json={"status": DealStatus.WON.value, "stage": DealStage.CLOSED.value},
|
||||
headers=headers,
|
||||
)
|
||||
assert patch_response.status_code == 200
|
||||
|
||||
refreshed = await client.get(
|
||||
"/api/v1/analytics/deals/summary?days=30",
|
||||
headers=headers,
|
||||
)
|
||||
assert refreshed.status_code == 200
|
||||
payload = refreshed.json()
|
||||
assert payload["won"]["count"] == 2
|
||||
Reference in New Issue
Block a user