ghost-shopper-api / python /tests /test_analytics_service.py
azzaraqi
Deploy FastAPI backend with Supabase pooler support
b84ea83
Raw
History Blame Contribute Delete
7.07 kB
"""
Unit Tests — AnalyticsService
Tests:
- get_summary: correct counts, completion rate calculation
- get_visit_analytics: per-visit stats, correct rates
- get_visit_analytics: returns None for missing visit
- get_visit_trends: returns trend points, respects days param
"""
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from models.visit_model import Visit
from models.checklist_model import Checklist
from models.photo_model import Photo
from services.analytics_service import AnalyticsService
@pytest.mark.asyncio
class TestAnalyticsSummary:
"""Tests for AnalyticsService.get_summary()"""
async def test_empty_db_returns_zeros(self, async_session: AsyncSession):
svc = AnalyticsService()
result = await svc.get_summary(async_session)
data = result["data"]
assert data["visits"]["total"] == 0
assert data["checklists"]["total_items"] == 0
assert data["checklists"]["completion_rate_pct"] == 0.0
assert data["photos"]["total"] == 0
async def test_counts_visits_by_status(self, async_session: AsyncSession):
from datetime import datetime, timezone
# Insert 2 visits with different statuses
for status in ("pending", "completed"):
async_session.add(Visit(
title=f"Visit {status}",
visit_date=datetime(2026, 5, 1, 9, 0, tzinfo=timezone.utc),
status=status,
))
await async_session.flush()
svc = AnalyticsService()
result = await svc.get_summary(async_session)
stats = result["data"]["visits"]
assert stats["total"] == 2
assert stats["by_status"]["pending"] == 1
assert stats["by_status"]["completed"] == 1
assert stats["by_status"]["in_progress"] == 0
async def test_checklist_completion_rate(
self, async_session: AsyncSession, sample_visit: Visit
):
# Add 4 items, 3 checked → 75%
from datetime import datetime, timezone
for i, checked in enumerate([True, True, True, False]):
async_session.add(Checklist(
visit_id=sample_visit.id,
item_name=f"Item {i}",
is_checked=checked,
))
await async_session.flush()
svc = AnalyticsService()
result = await svc.get_summary(async_session)
cl = result["data"]["checklists"]
assert cl["checked_items"] == 3
assert cl["total_items"] >= 4
assert cl["completion_rate_pct"] > 0.0
async def test_response_structure(
self, async_session: AsyncSession
):
svc = AnalyticsService()
result = await svc.get_summary(async_session)
assert result["status"] == "success"
assert "data" in result
assert "meta" in result
assert "generated_at" in result["meta"]
@pytest.mark.asyncio
class TestVisitAnalytics:
"""Tests for AnalyticsService.get_visit_analytics()"""
async def test_returns_none_for_missing_visit(
self, async_session: AsyncSession
):
import uuid
svc = AnalyticsService()
result = await svc.get_visit_analytics(async_session, uuid.uuid4())
assert result is None
async def test_visit_with_no_checklists(
self, async_session: AsyncSession, sample_visit: Visit
):
svc = AnalyticsService()
result = await svc.get_visit_analytics(async_session, sample_visit.id)
assert result is not None
cl = result["data"]["checklists"]
assert cl["total_items"] == 0
assert cl["completion_rate_pct"] == 0.0
async def test_partial_checklist_completion(
self, async_session: AsyncSession, sample_visit: Visit
):
# 1 checked, 1 unchecked → 50%
for checked in (True, False):
async_session.add(Checklist(
visit_id=sample_visit.id,
item_name="Item",
is_checked=checked,
))
await async_session.flush()
svc = AnalyticsService()
result = await svc.get_visit_analytics(async_session, sample_visit.id)
cl = result["data"]["checklists"]
assert cl["checked_items"] == 1
assert cl["unchecked_items"] == 1
assert cl["completion_rate_pct"] == 50.0
async def test_photo_count(
self, async_session: AsyncSession, sample_visit: Visit
):
from datetime import datetime, timezone
for _ in range(3):
async_session.add(Photo(
visit_id=sample_visit.id,
file_path="/test.jpg",
file_name="test.jpg",
))
await async_session.flush()
svc = AnalyticsService()
result = await svc.get_visit_analytics(async_session, sample_visit.id)
assert result["data"]["photos"]["total"] == 3
async def test_response_contains_visit_info(
self, async_session: AsyncSession, sample_visit: Visit
):
svc = AnalyticsService()
result = await svc.get_visit_analytics(async_session, sample_visit.id)
data = result["data"]
assert data["visit_id"] == str(sample_visit.id)
assert data["title"] == sample_visit.title
assert data["status"] == sample_visit.status
@pytest.mark.asyncio
class TestVisitTrends:
"""Tests for AnalyticsService.get_visit_trends()
NOTE: date_trunc() is PostgreSQL-specific.
These tests are skipped when running against SQLite (unit test DB).
They run against the real PostgreSQL in integration tests.
"""
@pytest.mark.skip(reason="date_trunc is PostgreSQL-only; covered by integration tests")
async def test_empty_returns_empty_list(self, async_session: AsyncSession):
svc = AnalyticsService()
result = await svc.get_visit_trends(async_session, days=7)
assert result["status"] == "success"
assert isinstance(result["data"]["trends"], list)
@pytest.mark.skip(reason="date_trunc is PostgreSQL-only; covered by integration tests")
async def test_period_days_reflected_in_response(
self, async_session: AsyncSession
):
svc = AnalyticsService()
result = await svc.get_visit_trends(async_session, days=90)
assert result["data"]["period_days"] == 90
@pytest.mark.skip(reason="date_trunc is PostgreSQL-only; covered by integration tests")
async def test_recent_visits_appear_in_trends(
self, async_session: AsyncSession
):
from datetime import datetime, timezone
v = Visit(
title="Today's visit",
visit_date=datetime.now(timezone.utc),
status="pending",
)
v.created_at = datetime.now(timezone.utc)
async_session.add(v)
await async_session.flush()
svc = AnalyticsService()
result = await svc.get_visit_trends(async_session, days=7)
trends = result["data"]["trends"]
assert len(trends) >= 1
total_count = sum(p["count"] for p in trends)
assert total_count >= 1