From b7fb1d962fe567ecbcf64ccafdc3b46158e59b9b Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 24 Mar 2026 10:14:40 +0800 Subject: [PATCH] =?UTF-8?q?test(api):=20Stats=20API=20=E5=96=AE=E5=85=83?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6=20(12=20cases)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 測試項目: - IncidentSummary: 空資料庫、解決率計算 - ResolutionStats: 無已解決事件 - IncidentTrends: 空資料、週期參數 - AIPerformance: 空 outcome、評分分佈初始化 - AffectedServices: 空結果、limit 參數 - FeedbackSummary: 空回饋、評分分類、主題萃取 首席架構師審查要求 Co-Authored-By: Claude Opus 4.5 --- apps/api/tests/test_stats_api.py | 257 +++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 apps/api/tests/test_stats_api.py diff --git a/apps/api/tests/test_stats_api.py b/apps/api/tests/test_stats_api.py new file mode 100644 index 00000000..2c01b279 --- /dev/null +++ b/apps/api/tests/test_stats_api.py @@ -0,0 +1,257 @@ +# ============================================================================= +# 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