# ============================================================================= # AWOOOI Statistics API 單元測試 # ============================================================================= # Phase 6.5: 測試統計分析 API 端點 # # 測試項目: # - /stats/incidents/summary # - /stats/incidents/resolution # - /stats/incidents/trends # - /stats/ai-performance # - /stats/services/affected # - /stats/feedback/summary # ============================================================================= import pytest from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch from src.api.v1.stats import ( get_incident_summary, get_resolution_stats, get_incident_trends, get_ai_performance, get_affected_services, get_feedback_summary, IncidentSummary, ResolutionStats, IncidentTrends, AIPerformance, ServiceImpact, FeedbackSummary, ) class TestIncidentSummary: """事件總覽統計測試""" @pytest.mark.asyncio async def test_empty_database_returns_zero_counts(self): """空資料庫應返回零計數""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( scalar=MagicMock(return_value=0), all=MagicMock(return_value=[]) )) result = await get_incident_summary(days=30, db=mock_db) assert result.total_incidents == 0 assert result.resolved_rate == 0.0 assert len(result.status_distribution) == 0 assert len(result.severity_distribution) == 0 @pytest.mark.asyncio async def test_resolved_rate_calculation(self): """解決率計算正確""" # 10 總數,3 已解決 = 30% mock_db = AsyncMock() call_count = 0 def mock_execute(*args, **kwargs): nonlocal call_count call_count += 1 result = MagicMock() if call_count == 1: # 總數 result.scalar.return_value = 10 elif call_count == 2: # 狀態分佈 result.all.return_value = [] elif call_count == 3: # 嚴重度分佈 result.all.return_value = [] elif call_count == 4: # 已解決數 result.scalar.return_value = 3 else: # 平均 signals result.scalar.return_value = 2.5 return result mock_db.execute = AsyncMock(side_effect=mock_execute) result = await get_incident_summary(days=30, db=mock_db) assert result.total_incidents == 10 assert result.resolved_rate == 30.0 class TestResolutionStats: """解決時間統計測試""" @pytest.mark.asyncio async def test_empty_results_return_none(self): """無已解決事件應返回 None""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_resolution_stats(days=30, db=mock_db) assert result.avg_minutes is None assert result.p50_minutes is None assert result.p95_minutes is None assert result.sample_size == 0 class TestIncidentTrends: """事件趨勢測試""" @pytest.mark.asyncio async def test_empty_data_returns_empty_list(self): """無資料應返回空列表""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_incident_trends(days=30, period="daily", db=mock_db) assert result.period == "daily" assert len(result.data) == 0 @pytest.mark.asyncio async def test_period_validation(self): """週期參數驗證""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) for period in ["daily", "weekly", "monthly"]: result = await get_incident_trends(days=30, period=period, db=mock_db) assert result.period == period class TestAIPerformance: """AI 效能統計測試""" @pytest.mark.asyncio async def test_empty_outcomes(self): """無 outcome 資料應返回零""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_ai_performance(days=30, db=mock_db) assert result.total_proposals == 0 assert result.executed_count == 0 assert result.execution_rate == 0.0 assert result.success_rate == 0.0 assert result.avg_effectiveness is None @pytest.mark.asyncio async def test_effectiveness_distribution_initialized(self): """有效性評分分佈初始化正確""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_ai_performance(days=30, db=mock_db) # 應該有 1-5 的所有評分 assert set(result.effectiveness_distribution.keys()) == {1, 2, 3, 4, 5} class TestAffectedServices: """受影響服務統計測試""" @pytest.mark.asyncio async def test_empty_results(self): """無資料應返回空列表""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_affected_services(days=30, limit=10, db=mock_db) assert len(result) == 0 @pytest.mark.asyncio async def test_limit_parameter(self): """limit 參數應限制結果數量""" mock_db = AsyncMock() # 模擬 5 個服務的資料 mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[ (["svc1"], "P0"), (["svc2"], "P1"), (["svc3"], "P2"), (["svc1"], "P0"), # svc1 重複 (["svc2"], "P1"), # svc2 重複 ]) )) result = await get_affected_services(days=30, limit=2, db=mock_db) # 應該只返回前 2 個 (按計數排序) assert len(result) <= 2 class TestFeedbackSummary: """人類回饋摘要測試""" @pytest.mark.asyncio async def test_empty_feedback(self): """無回饋應返回零計數""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[]) )) result = await get_feedback_summary(days=30, db=mock_db) assert result.total_feedback == 0 assert result.positive_count == 0 assert result.neutral_count == 0 assert result.negative_count == 0 assert len(result.common_themes) == 0 @pytest.mark.asyncio async def test_score_categorization(self): """評分分類正確 (>=4 正面, =3 中性, <=2 負面)""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[ ({"effectiveness_score": 5},), # 正面 ({"effectiveness_score": 4},), # 正面 ({"effectiveness_score": 3},), # 中性 ({"effectiveness_score": 2},), # 負面 ({"effectiveness_score": 1},), # 負面 ]) )) result = await get_feedback_summary(days=30, db=mock_db) assert result.positive_count == 2 assert result.neutral_count == 1 assert result.negative_count == 2 assert result.total_feedback == 5 @pytest.mark.asyncio async def test_theme_extraction(self): """主題萃取包含關鍵字""" mock_db = AsyncMock() mock_db.execute = AsyncMock(return_value=MagicMock( all=MagicMock(return_value=[ ({"learning_notes": "Connection timeout issue"},), ({"learning_notes": "Memory leak detected"},), ({"learning_notes": "timeout again"},), ]) )) result = await get_feedback_summary(days=30, db=mock_db) # timeout 出現 2 次,應該在常見主題中 assert "timeout" in result.common_themes or "connection" in result.common_themes