"""E-1 월별 리포트 자동 생성 테스트""" import sys, ast, os, json, re, hashlib from datetime import datetime from calendar import monthrange os.environ.setdefault("GUARDIA_SECRET_KEY", "test-e1-secret-key-32bytes-padded!") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_e1.db") ok = True print("=== 1. 구문 검사 ===") files = ["routers/report.py", "main.py"] for f in files: try: with open(f, encoding="utf-8") as fh: src = fh.read() ast.parse(src) print(f" OK {f}") except SyntaxError as e: print(f" ERR {f}: {e}") ok = False print("\n=== 2. routers/report.py 엔드포인트 확인 ===") with open("routers/report.py", encoding="utf-8") as f: report_src = f.read() checks = [ ('@router.get("/generate"', "GET /generate 리포트 즉시 생성"), ('@router.get("/monthly/{year}/{month}"', "GET /monthly/{year}/{month}"), ('@router.get("/list"', "GET /list 목록"), ('@router.get("/preview"', "GET /preview 미리보기"), ('@router.get("/{report_id}"', "GET /{report_id} 상세"), ('@router.get("/export/{report_id}"', "GET /export/{report_id} 다운로드"), ('@router.post("/schedule"', "POST /schedule 스케줄 설정"), ("generate_monthly_report", "generate_monthly_report 함수"), ("_gather_sr_stats", "_gather_sr_stats 통계 함수"), ("_gather_audit_stats", "_gather_audit_stats 감사 함수"), ("_gather_capacity_stats", "_gather_capacity_stats 용량 함수"), ("_llm_generate_summary", "_llm_generate_summary LLM 함수"), ("_build_fallback_summary", "_build_fallback_summary 규칙 요약"), ("_build_recommendations", "_build_recommendations 권고사항"), ("localhost:11434", "Ollama 내부 LLM (외부 API 금지 준수)"), ("StreamingResponse", "StreamingResponse (JSON 다운로드)"), ("Content-Disposition", "Content-Disposition 헤더"), ("UserRole.ADMIN", "ADMIN 권한 제한"), ("UserRole.PM", "PM 권한 제한"), ("ScheduleConfigIn", "ScheduleConfigIn 스키마"), ("health_score", "health_score 헬스 스코어"), ("health_grade", "health_grade 등급"), ("_reports", "인메모리 리포트 캐시"), ("RPT-", "RPT- 리포트 ID 포맷"), ("resolution_rate", "resolution_rate SR 해결률"), ("executive_summary", "executive_summary 경영진 요약"), ("recommendations", "recommendations 권고사항"), ] for sym, desc in checks: status = "OK" if sym in report_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 3. main.py E-1 라우터 등록 확인 ===") with open("main.py", encoding="utf-8") as f: main_src = f.read() main_checks = [ ("report," in main_src or "report\n" in main_src, "report 임포트"), ("report.router" in main_src, "report.router 등록"), ("E-1" in main_src, "E-1 주석"), ] for check, desc in main_checks: status = "OK" if check else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 4. 리포트 ID 포맷 검증 ===") try: year, month = 2026, 5 requester = "admin" report_id = f"RPT-{year}{month:02d}-{hashlib.sha256(f'{year}{month}{requester}'.encode()).hexdigest()[:6].upper()}" assert report_id.startswith("RPT-"), f"RPT- 접두사 없음: {report_id}" assert len(report_id) == 17, f"ID 길이 오류: {len(report_id)} (기대: 17)" assert re.match(r"RPT-\d{6}-[A-F0-9]{6}", report_id), f"포맷 불일치: {report_id}" print(f" OK 리포트 ID 생성: {report_id}") print(f" OK ID 길이: {len(report_id)}자 (기대: 16)") print(f" OK 포맷 정규식 통과: RPT-YYYYMM-XXXXXX") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 5. 헬스 스코어 알고리즘 검증 ===") try: def calc_health(critical_events, critical_plans, resolution_rate): health_deductions = ( min(30, critical_events * 5) + min(20, critical_plans * 10) + max(0, 20 - resolution_rate // 5) ) return max(0, 100 - int(health_deductions)) # 완벽한 운영 score_perfect = calc_health(0, 0, 100) assert score_perfect == 100, f"완벽 점수 오류: {score_perfect}" print(f" OK 완벽 운영: 헬스 스코어 = {score_perfect} (기대: 100)") # CRITICAL 이벤트 6건 → -30 score_critical = calc_health(6, 0, 100) assert score_critical == 70, f"CRITICAL 6건 점수 오류: {score_critical}" print(f" OK CRITICAL 6건: 헬스 스코어 = {score_critical} (기대: 70)") # 용량 위험 2개 → -20 score_capacity = calc_health(0, 2, 100) assert score_capacity == 80, f"용량 위험 2개 점수 오류: {score_capacity}" print(f" OK 용량 위험 2개: 헬스 스코어 = {score_capacity} (기대: 80)") # SR 해결률 0% → -20 score_sr = calc_health(0, 0, 0) assert score_sr == 80, f"SR 해결률 0% 점수 오류: {score_sr}" print(f" OK SR 해결률 0%: 헬스 스코어 = {score_sr} (기대: 80)") # 최악 시나리오 (최대 감점 30+20+20=70 → 최소 30) score_worst = calc_health(100, 100, 0) assert score_worst == 30, f"최악 시나리오 점수 오류: {score_worst} (기대: 30)" print(f" OK 최악 시나리오: 헬스 스코어 = {score_worst} (기대: 30, 최대 감점=70)") # 등급 구분 grade_tests = [ (95, "A"), (90, "A"), (80, "B"), (75, "B"), (65, "C"), (60, "C"), (50, "D"), ] for score, expected in grade_tests: grade = "A" if score >= 90 else "B" if score >= 75 else "C" if score >= 60 else "D" assert grade == expected, f"등급 오류: score={score} grade={grade} (기대: {expected})" print(f" OK 등급 구분 (A/B/C/D) 모두 정확") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 6. 권고사항 생성 로직 검증 ===") try: def build_recommendations(stats): recs = [] sr = stats.get("sr", {}) sec = stats.get("security", {}) cap = stats.get("capacity", {}) if sr.get("resolution_rate", 100) < 80: recs.append(f"SR 해결률 미달") if sr.get("open", 0) > 10: recs.append(f"미처리 SR 다수") if sec.get("critical_events", 0) > 0: recs.append(f"CRITICAL 보안 이벤트 조사 필요") if cap.get("critical_plans", 0) > 0: recs.append(f"용량 위험 시스템 증설 권고") if not recs: recs.append("운영 지표 정상") return recs # 정상 운영 → 정상 메시지 recs_ok = build_recommendations({"sr": {"resolution_rate": 95, "open": 3}, "security": {"critical_events": 0}, "capacity": {"critical_plans": 0}}) assert len(recs_ok) == 1 and "정상" in recs_ok[0], f"정상 권고 오류: {recs_ok}" print(f" OK 정상 운영: '{recs_ok[0]}'") # 문제 다수 → 다건 권고 recs_multi = build_recommendations({"sr": {"resolution_rate": 60, "open": 15}, "security": {"critical_events": 2}, "capacity": {"critical_plans": 1}}) assert len(recs_multi) == 4, f"다건 권고 개수 오류: {len(recs_multi)} (기대: 4)" print(f" OK 문제 다수: 권고 {len(recs_multi)}건 생성") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 7. Fallback 요약 생성 검증 ===") try: def build_fallback(stats, year, month): sr = stats.get("sr", {}) sec = stats.get("security", {}) cap = stats.get("capacity", {}) lines = [ f"{year}년 {month}월 GUARDiA ITSM 운영 월간 보고서입니다.", f"이번 달 총 SR {sr.get('total', 0)}건이 접수되어 해결률 {sr.get('resolution_rate', 0)}%를 달성했습니다.", f"보안 감사 이벤트는 총 {sec.get('total_events', 0)}건이며, " f"중요(CRITICAL) 이벤트는 {sec.get('critical_events', 0)}건입니다.", ] if cap.get("critical_plans", 0) > 0: lines.append(f"용량 위험 시스템이 {cap['critical_plans']}개 감지되어 즉각적인 조치가 필요합니다.") return " ".join(lines) summary = build_fallback({ "sr": {"total": 120, "resolution_rate": 92.5}, "security": {"total_events": 450, "critical_events": 0}, "capacity": {"critical_plans": 0}, }, 2026, 5) assert "2026년 5월" in summary, "연월 포함 오류" assert "120건" in summary, "SR 건수 포함 오류" assert "92.5%" in summary, "해결률 포함 오류" print(f" OK Fallback 요약 생성 성공") print(f" OK 내용: {summary[:80]}...") # 용량 위험 포함 summary2 = build_fallback({ "sr": {"total": 50, "resolution_rate": 70.0}, "security": {"total_events": 100, "critical_events": 3}, "capacity": {"critical_plans": 2}, }, 2026, 5) assert "용량 위험" in summary2, "용량 위험 문구 포함 오류" print(f" OK 용량 위험 시스템 언급 확인") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 8. ScheduleConfigIn 스키마 확인 ===") schedule_checks = [ ("send_day", "send_day 필드"), ("recipients", "recipients 필드"), ("include_llm", "include_llm 필드"), ("enabled", "enabled 필드"), ("1 <= body.send_day <= 28", "send_day 범위 검증 (1~28)"), ] for sym, desc in schedule_checks: status = "OK" if sym in report_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 9. 보안 정책 확인 ===") security_checks = [ ("localhost:11434" in report_src and "openai" not in report_src.lower() and "anthropic" not in report_src.lower(), "외부 AI API 미사용 (Ollama only)"), ("UserRole.ADMIN" in report_src, "ADMIN 전용 기능 존재"), ("attachment; filename=" in report_src, "다운로드 파일명 설정"), ("ensure_ascii=False" in report_src, "한글 JSON 인코딩"), ] for check, desc in security_checks: status = "OK" if check else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 10. 캘린더 유틸리티 검증 ===") try: # monthrange 정확도 for m, expected_last in [(1, 31), (2, 28), (4, 30), (12, 31)]: _, last = monthrange(2026, m) assert last == expected_last, f"{m}월 마지막 날 오류: {last} (기대: {expected_last})" # 2024년 2월은 윤년 _, last_feb_2024 = monthrange(2024, 2) assert last_feb_2024 == 29, f"2024년 2월 오류: {last_feb_2024}" print(f" OK monthrange 정확도 검증 (윤년 포함)") # 기간 범위 생성 검증 year, month = 2026, 5 _, last_day = monthrange(year, month) start_dt = datetime(year, month, 1, 0, 0, 0) end_dt = datetime(year, month, last_day, 23, 59, 59) assert start_dt.day == 1, "시작일 오류" assert end_dt.day == 31, "종료일 오류 (5월)" assert (end_dt - start_dt).days == 30, "기간 오류" print(f" OK 기간 범위: {start_dt.date()} ~ {end_dt.date()}") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== E-1 월별 리포트 자동 생성 테스트 완료 ===") if ok: print("모든 검사 통과") else: sys.exit(1)