Spaces:
Sleeping
Sleeping
| """ | |
| 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 | |
| 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"] | |
| 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 | |
| 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. | |
| """ | |
| 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) | |
| 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 | |
| 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 | |