152 lines
5.5 KiB
Python
152 lines
5.5 KiB
Python
"""
|
||
AlertGroupingService 單元測試
|
||
==============================
|
||
ADR-076: 告警聚合引擎 — 告警風暴防禦
|
||
|
||
🔴🔴 遵循「禁止 Mock 測試鐵律」
|
||
- build_group_key / GroupingResult 邏輯測試:純 Python,無需 Redis
|
||
- Redis 整合部分標記 @pytest.mark.integration,正常 CI 跳過
|
||
|
||
建立: 2026-04-14 (台北時區) Claude Haiku 4.5
|
||
"""
|
||
|
||
import pytest
|
||
|
||
from src.services.alert_grouping_service import (
|
||
AlertGroupingService,
|
||
GroupingResult,
|
||
_decode_redis_member,
|
||
)
|
||
|
||
|
||
class TestBuildGroupKey:
|
||
"""測試聚合分組 key 生成邏輯"""
|
||
|
||
def test_basic_key(self):
|
||
"""基本 alertname + namespace → group_key"""
|
||
key = AlertGroupingService.build_group_key("PodCrashLoopBackOff", "awoooi-prod")
|
||
assert key == "PodCrashLoopBackOff:awoooi-prod"
|
||
|
||
def test_strips_numeric_suffix(self):
|
||
"""帶數字後綴的 alertname 應取前綴"""
|
||
key = AlertGroupingService.build_group_key("PodCrashLoopBackOff-3", "awoooi-prod")
|
||
assert key == "PodCrashLoopBackOff:awoooi-prod"
|
||
|
||
def test_strips_long_numeric_suffix(self):
|
||
"""帶長數字後綴的 alertname 應取前綴"""
|
||
key = AlertGroupingService.build_group_key("HostHighCpuLoad-1234567", "default")
|
||
assert key == "HostHighCpuLoad:default"
|
||
|
||
def test_same_prefix_same_key(self):
|
||
"""相同前綴、相同 namespace → 相同 group_key(聚合生效)"""
|
||
key1 = AlertGroupingService.build_group_key("PodOOMKilled-1", "awoooi-prod")
|
||
key2 = AlertGroupingService.build_group_key("PodOOMKilled-2", "awoooi-prod")
|
||
key3 = AlertGroupingService.build_group_key("PodOOMKilled-3", "awoooi-prod")
|
||
assert key1 == key2 == key3
|
||
|
||
def test_different_namespace_different_key(self):
|
||
"""相同 alertname、不同 namespace → 不同 group_key"""
|
||
key1 = AlertGroupingService.build_group_key("PodCrash", "awoooi-prod")
|
||
key2 = AlertGroupingService.build_group_key("PodCrash", "awoooi-staging")
|
||
assert key1 != key2
|
||
|
||
def test_different_alertname_different_key(self):
|
||
"""不同 alertname、相同 namespace → 不同 group_key"""
|
||
key1 = AlertGroupingService.build_group_key("PodCrash", "awoooi-prod")
|
||
key2 = AlertGroupingService.build_group_key("HostHighCpu", "awoooi-prod")
|
||
assert key1 != key2
|
||
|
||
def test_empty_namespace(self):
|
||
"""namespace 為空字串時應正常處理"""
|
||
key = AlertGroupingService.build_group_key("PodCrash", "")
|
||
assert key == "PodCrash:"
|
||
|
||
def test_no_suffix_unchanged(self):
|
||
"""無數字後綴的 alertname 應保持不變"""
|
||
key = AlertGroupingService.build_group_key("HostHighCpuLoad", "default")
|
||
assert key == "HostHighCpuLoad:default"
|
||
|
||
|
||
class TestGroupingResultDataclass:
|
||
"""測試 GroupingResult dataclass"""
|
||
|
||
def test_child_alert(self):
|
||
"""子告警:is_grouped=True, is_parent=False"""
|
||
result = GroupingResult(
|
||
is_grouped=True,
|
||
group_key="PodCrash:awoooi-prod",
|
||
count=5,
|
||
parent_fingerprint="fp-001",
|
||
is_parent=False,
|
||
)
|
||
assert result.is_grouped is True
|
||
assert result.is_parent is False
|
||
assert result.count == 5
|
||
|
||
def test_parent_alert(self):
|
||
"""父告警:is_grouped=False, is_parent=True"""
|
||
result = GroupingResult(
|
||
is_grouped=False,
|
||
group_key="PodCrash:awoooi-prod",
|
||
count=1,
|
||
parent_fingerprint="fp-001",
|
||
is_parent=True,
|
||
)
|
||
assert result.is_grouped is False
|
||
assert result.is_parent is True
|
||
|
||
def test_second_alert_is_now_grouped(self):
|
||
"""門檻為 2:第一張保留主卡,第二張同組告警開始收斂。"""
|
||
result = GroupingResult(
|
||
is_grouped=True,
|
||
group_key="PodCrash:awoooi-prod",
|
||
count=2,
|
||
parent_fingerprint="fp-001",
|
||
is_parent=False,
|
||
)
|
||
assert result.is_grouped is True
|
||
|
||
def test_group_key_format(self):
|
||
"""group_key 格式應為 {alertname_prefix}:{namespace}"""
|
||
result = GroupingResult(
|
||
is_grouped=True,
|
||
group_key="PodOOMKilled:awoooi-prod",
|
||
count=4,
|
||
parent_fingerprint=None,
|
||
is_parent=False,
|
||
)
|
||
assert ":" in result.group_key
|
||
parts = result.group_key.split(":")
|
||
assert len(parts) == 2
|
||
|
||
|
||
class TestRedisMemberDecode:
|
||
"""測試 Redis zrange member 正規化。"""
|
||
|
||
def test_decode_bytes_member(self):
|
||
assert _decode_redis_member(b"fp-parent", "fallback") == "fp-parent"
|
||
|
||
def test_decode_none_uses_fallback(self):
|
||
assert _decode_redis_member(None, "fallback") == "fallback"
|
||
|
||
|
||
class TestAlertGroupingServiceConstants:
|
||
"""測試服務常量設定"""
|
||
|
||
def test_window_seconds(self):
|
||
"""視窗應為 5 分鐘 (300 秒)"""
|
||
assert AlertGroupingService.WINDOW_SECONDS == 300
|
||
|
||
def test_group_threshold(self):
|
||
"""聚合閾值應為 2:保留第一張主卡,第二張同組告警開始收斂。"""
|
||
assert AlertGroupingService.GROUP_THRESHOLD == 2
|
||
|
||
def test_ttl_seconds(self):
|
||
"""TTL 應長於視窗"""
|
||
assert AlertGroupingService.TTL_SECONDS > AlertGroupingService.WINDOW_SECONDS
|
||
|
||
def test_redis_key_prefix(self):
|
||
"""Redis key 前綴應符合規範"""
|
||
assert AlertGroupingService.PREFIX_WINDOW.startswith("alert_group:")
|
||
assert AlertGroupingService.PREFIX_META.startswith("alert_group:")
|