zioinfo-mail/workspace/guardia-itsm/test_e1_report.py
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

273 lines
12 KiB
Python

"""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)