test(api): Stats API 單元測試 (12 cases)

測試項目:
- IncidentSummary: 空資料庫、解決率計算
- ResolutionStats: 無已解決事件
- IncidentTrends: 空資料、週期參數
- AIPerformance: 空 outcome、評分分佈初始化
- AffectedServices: 空結果、limit 參數
- FeedbackSummary: 空回饋、評分分類、主題萃取

首席架構師審查要求

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-24 10:14:40 +08:00
parent 290e4a53eb
commit b7fb1d962f

View File

@@ -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