""" 단위 테스트: ChatOps 확장 + 예측 장애 방지 커버 항목: - chatops_extended 라우터 임포트 및 prefix 검증 - predictive_failure 라우터 임포트 및 prefix 검증 - ORM 모델 테이블명 및 컬럼 검증 (ChatOpsCommand, FailureSignal, PreventionAction) - 명령어 파서 (_parse_command) 단위 검증 - 리스크 점수 계산 (_calc_risk_score) 단위 검증 - 예측 장애 레이블 (_predict_failure_label) 검증 - 지원 채널 정의 일관성 검증 - 장애 패턴 모델 정의 검증 """ from __future__ import annotations import pytest # ══════════════════════════════════════════════════════════════════════════════ # chatops_extended 라우터 검증 # ══════════════════════════════════════════════════════════════════════════════ def test_chatops_extended_import(): """chatops_extended 모듈이 오류 없이 임포트된다.""" from routers import chatops_extended assert chatops_extended.router is not None def test_chatops_router_prefix(): """라우터 prefix가 /api/chatops인지 확인.""" from routers.chatops_extended import router assert router.prefix == "/api/chatops" def test_chatops_supported_channels(): """지원 채널 3종(kakao, slack, internal)이 정의되어 있다.""" from routers.chatops_extended import SUPPORTED_CHANNELS assert "kakao" in SUPPORTED_CHANNELS assert "slack" in SUPPORTED_CHANNELS assert "internal" in SUPPORTED_CHANNELS # 각 채널에 enabled 키가 있어야 한다 for ch_id, info in SUPPORTED_CHANNELS.items(): assert "enabled" in info assert "name" in info def test_chatops_command_definitions(): """지원 명령어가 7개 이상 정의되어 있다.""" from routers.chatops_extended import COMMAND_DEFINITIONS commands = [d["command"] for d in COMMAND_DEFINITIONS] assert len(commands) >= 7 # 필수 명령어 포함 여부 assert "/sr create" in commands assert "/status" in commands assert "/deploy" in commands assert "/approve" in commands assert "/report" in commands assert "/patch" in commands assert "/workflow" in commands def test_parse_command_slash_sr_create(): """'/sr create 서버 재시작 본문' 파싱 결과 확인.""" from routers.chatops_extended import _parse_command result = _parse_command("/sr create web-01 재시작 요청") assert result is not None assert result["command"] == "/sr create" assert "web-01" in result["args"] def test_parse_command_status_with_id(): """'/status SR-2026-001' 파싱 결과 확인.""" from routers.chatops_extended import _parse_command result = _parse_command("/status SR-2026-001") assert result is not None assert result["command"] == "/status" def test_parse_command_unknown_returns_dict(): """인식되지 않는 명령어도 dict를 반환한다 (None 반환 없음).""" from routers.chatops_extended import _parse_command result = _parse_command("/unknown_cmd arg1 arg2") assert result is not None assert "command" in result def test_parse_command_no_slash_returns_none(): """슬래시 없는 일반 메시지는 None을 반환한다.""" from routers.chatops_extended import _parse_command result = _parse_command("안녕하세요 도움이 필요합니다") assert result is None def test_parse_command_empty_string_returns_none(): """빈 문자열은 None을 반환한다.""" from routers.chatops_extended import _parse_command result = _parse_command("") assert result is None # ══════════════════════════════════════════════════════════════════════════════ # predictive_failure 라우터 검증 # ══════════════════════════════════════════════════════════════════════════════ def test_predictive_failure_import(): """predictive_failure 모듈이 오류 없이 임포트된다.""" from routers import predictive_failure assert predictive_failure.router is not None def test_predictive_failure_router_prefix(): """라우터 prefix가 /api/predict-fail인지 확인.""" from routers.predictive_failure import router assert router.prefix == "/api/predict-fail" def test_failure_patterns_defined(): """장애 패턴 모델이 4종 이상 정의되어 있다.""" from routers.predictive_failure import FAILURE_PATTERNS assert len(FAILURE_PATTERNS) >= 4 signal_types = {p["signal_type"] for p in FAILURE_PATTERNS} assert "cpu_spike" in signal_types assert "mem_leak" in signal_types assert "disk_full" in signal_types assert "error_rate" in signal_types def test_failure_pattern_schema(): """각 패턴 모델에 필수 키가 존재한다.""" from routers.predictive_failure import FAILURE_PATTERNS required_keys = {"id", "signal_type", "name", "description", "threshold", "window_days", "algorithm"} for p in FAILURE_PATTERNS: for key in required_keys: assert key in p, f"패턴 '{p.get('id', '?')}'에 '{key}' 키 누락" def test_prevention_templates_coverage(): """예방 조치 템플릿이 4종 신호 유형을 모두 커버한다.""" from routers.predictive_failure import PREVENTION_TEMPLATES for sig_type in ("cpu_spike", "mem_leak", "disk_full", "error_rate"): assert sig_type in PREVENTION_TEMPLATES tpl = PREVENTION_TEMPLATES[sig_type] assert "action_type" in tpl assert "action_cmd" in tpl assert "description" in tpl def test_calc_risk_score_below_threshold(): """임계값 미만 값에서 리스크 점수가 0.8 이하이다.""" from routers.predictive_failure import _calc_risk_score score = _calc_risk_score(70.0, 85.0, "cpu_spike") assert 0.0 <= score <= 1.0 # 임계값 미만이므로 1.0 미만이어야 함 assert score < 1.0 def test_calc_risk_score_above_threshold(): """임계값을 초과하면 리스크 점수가 높다 (0.5 초과).""" from routers.predictive_failure import _calc_risk_score score = _calc_risk_score(95.0, 85.0, "cpu_spike") assert score > 0.5 def test_calc_risk_score_disk_full_high_weight(): """disk_full 신호는 가중치 1.0이므로 다른 타입 대비 높다.""" from routers.predictive_failure import _calc_risk_score disk_score = _calc_risk_score(95.0, 95.0, "disk_full") cpu_score = _calc_risk_score(95.0, 95.0, "cpu_spike") # disk_full(1.0) >= cpu_spike(0.8) assert disk_score >= cpu_score def test_calc_risk_score_zero_threshold(): """임계값이 0이면 리스크 점수 0.0 반환 (ZeroDivision 없음).""" from routers.predictive_failure import _calc_risk_score score = _calc_risk_score(50.0, 0.0, "cpu_spike") assert score == 0.0 def test_predict_failure_label_low_risk(): """리스크 점수 0.3 미만은 None 반환 (장애 예측 없음).""" from routers.predictive_failure import _predict_failure_label label = _predict_failure_label("cpu_spike", 0.3) assert label is None def test_predict_failure_label_high_risk(): """리스크 점수 0.7 이상은 레이블 문자열 반환.""" from routers.predictive_failure import _predict_failure_label label = _predict_failure_label("mem_leak", 0.8) assert isinstance(label, str) assert len(label) > 0 def test_predict_failure_label_disk_full(): """disk_full 신호의 레이블에 '디스크' 또는 '쓰기' 포함.""" from routers.predictive_failure import _predict_failure_label label = _predict_failure_label("disk_full", 0.9) assert label is not None assert any(kw in label for kw in ("디스크", "쓰기", "Disk", "Full")) # ══════════════════════════════════════════════════════════════════════════════ # ORM 모델 검증 # ══════════════════════════════════════════════════════════════════════════════ def test_chatops_command_orm(): """ChatOpsCommand ORM 모델의 테이블명과 컬럼을 확인한다.""" from models import ChatOpsCommand assert ChatOpsCommand.__tablename__ == "tb_chatops_command" cols = {c.name for c in ChatOpsCommand.__table__.columns} for col in ("id", "channel", "command", "args", "user_id", "response", "success", "created_at"): assert col in cols, f"ChatOpsCommand에 '{col}' 컬럼 누락" def test_failure_signal_orm(): """FailureSignal ORM 모델의 테이블명과 컬럼을 확인한다.""" from models import FailureSignal assert FailureSignal.__tablename__ == "tb_failure_signal" cols = {c.name for c in FailureSignal.__table__.columns} for col in ("id", "server_name", "signal_type", "value", "threshold", "risk_score", "predicted_failure", "created_at"): assert col in cols, f"FailureSignal에 '{col}' 컬럼 누락" def test_prevention_action_orm(): """PreventionAction ORM 모델의 테이블명과 컬럼을 확인한다.""" from models import PreventionAction assert PreventionAction.__tablename__ == "tb_prevention_action" cols = {c.name for c in PreventionAction.__table__.columns} for col in ("id", "signal_id", "action_type", "action_cmd", "success", "created_at"): assert col in cols, f"PreventionAction에 '{col}' 컬럼 누락" def test_prevention_action_fk_signal(): """PreventionAction.signal_id가 tb_failure_signal을 참조한다.""" from models import PreventionAction fk_targets = { str(fk.column) for fk in PreventionAction.__table__.foreign_keys } assert any("tb_failure_signal" in t for t in fk_targets)