""" CS2 規則引擎自動執行條件邏輯測試 ================================== 測試範圍:webhooks.py CS2 路徑的 _can_auto 條件判斷邏輯 設計原則: - 不測試 DB/K8s 端點(屬 integration test 範疇) - 直接測試 _can_auto 的五個安全條件是否正確 - 從 auto_approve 導入真實的 _DESTRUCTIVE_PATTERNS(不 mock) 安全防線驗證: 1. rule_kubectl 空字串 → False 2. CRITICAL risk → False 3. DESTRUCTIVE_PATTERNS 命中 → False 4. NO_ACTION 在 rule_action → False 5. 全條件滿足 → True 建立:2026-04-27 ogt + Claude Sonnet 4.6 (台北時區) """ from src.models.approval import RiskLevel from src.services.auto_approve import _DESTRUCTIVE_PATTERNS def _evaluate_can_auto( rule_kubectl: str, rule_risk: RiskLevel, rule_action: str, ) -> bool: """ 複製 webhooks.py CS2 路徑的 _can_auto 邏輯,用於單元測試。 任何修改 webhooks.py 邏輯的人,必須同步更新此函數。 """ _destructive_set = set(p.lower() for p in _DESTRUCTIVE_PATTERNS) return ( bool(rule_kubectl) and rule_risk != RiskLevel.CRITICAL and not any(p in rule_kubectl.lower() for p in _destructive_set) and "NO_ACTION" not in rule_action ) class TestCS2CanAutoConditions: """驗證 CS2 _can_auto 五個安全防線""" # ── 正向:全條件滿足 ───────────────────────────────────────────────── def test_all_conditions_met_returns_true(self): """kubectl 非空 + medium risk + 無破壞性 + 非 NO_ACTION → True""" assert _evaluate_can_auto( rule_kubectl="kubectl rollout restart deployment/api -n prod", rule_risk=RiskLevel.MEDIUM, rule_action="重啟 API Deployment | kubectl rollout restart deployment/api -n prod", ) is True def test_low_risk_returns_true(self): assert _evaluate_can_auto( rule_kubectl="kubectl rollout restart deployment/worker -n prod", rule_risk=RiskLevel.LOW, rule_action="重啟 Worker | kubectl rollout restart deployment/worker -n prod", ) is True # ── 防線 1:kubectl 空字串 ───────────────────────────────────────────── def test_empty_kubectl_returns_false(self): assert _evaluate_can_auto( rule_kubectl="", rule_risk=RiskLevel.LOW, rule_action="NO_ACTION - 主機記憶體觀察", ) is False def test_whitespace_only_kubectl_returns_false(self): # webhooks.py line 1265: rule_kubectl = str(...).strip() # 因此 whitespace-only 在到達 _can_auto 前已被 strip 為空字串 # 此測試驗證調用方的前置處理行為 assert _evaluate_can_auto( rule_kubectl="", # strip() 後的結果 rule_risk=RiskLevel.LOW, rule_action="重啟 | 某指令", ) is False # ── 防線 2:CRITICAL risk ───────────────────────────────────────────── def test_critical_risk_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl rollout restart deployment/db -n prod", rule_risk=RiskLevel.CRITICAL, rule_action="重啟 DB | kubectl rollout restart deployment/db -n prod", ) is False # ── 防線 3:DESTRUCTIVE_PATTERNS ──────────────────────────────────── def test_delete_pod_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl delete pod api-xxx-yyy -n prod", rule_risk=RiskLevel.LOW, rule_action="刪除 Pod | kubectl delete pod api-xxx-yyy -n prod", ) is False def test_delete_pods_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl delete pods --all -n prod", rule_risk=RiskLevel.LOW, rule_action="刪除所有 Pod | kubectl delete pods --all -n prod", ) is False def test_scale_to_zero_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl scale deployment/api --replicas=0 -n prod", rule_risk=RiskLevel.LOW, rule_action="縮容 | kubectl scale deployment/api --replicas=0", ) is False def test_kubectl_drain_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl drain node-1 --ignore-daemonsets", rule_risk=RiskLevel.MEDIUM, rule_action="驅逐節點 | kubectl drain node-1", ) is False def test_rollout_undo_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl rollout undo deployment/api -n prod", rule_risk=RiskLevel.MEDIUM, rule_action="回滾 | kubectl rollout undo deployment/api", ) is False def test_destructive_pattern_case_insensitive(self): """大寫 DELETE POD 也必須攔截""" assert _evaluate_can_auto( rule_kubectl="kubectl DELETE POD api-xxx -n prod", rule_risk=RiskLevel.LOW, rule_action="刪除 | kubectl DELETE POD", ) is False # ── 防線 4:NO_ACTION ──────────────────────────────────────────────── def test_no_action_in_action_string_returns_false(self): assert _evaluate_can_auto( rule_kubectl="kubectl get pods -n prod", rule_risk=RiskLevel.LOW, rule_action="NO_ACTION - 觀察主機狀態", ) is False def test_no_action_prefix_returns_false(self): assert _evaluate_can_auto( rule_kubectl="", rule_risk=RiskLevel.LOW, rule_action="NO_ACTION - 主機記憶體正常", ) is False # ── 邊界:_DESTRUCTIVE_PATTERNS 非空 ──────────────────────────────── def test_destructive_patterns_list_not_empty(self): """確保 _DESTRUCTIVE_PATTERNS 未被意外清空""" assert len(_DESTRUCTIVE_PATTERNS) > 0 # ── 組合:多個防線同時觸發 ─────────────────────────────────────────── def test_critical_and_destructive_both_block(self): """CRITICAL + destructive,任一條件都應攔截""" assert _evaluate_can_auto( rule_kubectl="kubectl delete deployment/api -n prod", rule_risk=RiskLevel.CRITICAL, rule_action="刪除 Deployment | kubectl delete deployment/api", ) is False