測試項目: - IncidentSummary: 空資料庫、解決率計算 - ResolutionStats: 無已解決事件 - IncidentTrends: 空資料、週期參數 - AIPerformance: 空 outcome、評分分佈初始化 - AffectedServices: 空結果、limit 參數 - FeedbackSummary: 空回饋、評分分類、主題萃取 首席架構師審查要求 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
258 lines
8.2 KiB
Python
258 lines
8.2 KiB
Python
# =============================================================================
|
||
# 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
|