"""C-2 변경 관리 CAB 테스트""" import sys, ast, os os.environ.setdefault("GUARDIA_SECRET_KEY", "test-c2-secret-key-32bytes-padded!") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_c2.db") ok = True print("=== 1. 구문 검사 ===") files = ["routers/change.py", "models.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. models.py CAB 모델 확인 ===") with open("models.py", encoding="utf-8") as f: models_src = f.read() model_checks = [ ("class RFChange(Base):", "RFChange ORM 클래스 (RFC)"), ("class CABVote(Base):", "CABVote ORM 클래스"), ("class FreezeWindow(Base):", "FreezeWindow ORM 클래스"), ("class RFCStatus(str, Enum):", "RFCStatus Enum"), ("class ChangeType(str, Enum):", "ChangeType Enum"), ("class ChangeRisk(str, Enum):", "ChangeRisk Enum"), ("class CABVoteResult(str, Enum):", "CABVoteResult Enum"), ("RFChangeOut", "RFChangeOut Pydantic"), ("RFChangeCreate", "RFChangeCreate Pydantic"), ("CABVoteCreate", "CABVoteCreate Pydantic"), ("FreezeWindowOut", "FreezeWindowOut Pydantic"), ("FreezeWindowCreate", "FreezeWindowCreate Pydantic"), ("tb_rfc", "tb_rfc 테이블명"), ("tb_cab_vote", "tb_cab_vote 테이블명"), ("tb_freeze_window", "tb_freeze_window 테이블명"), ("rollback_plan", "롤백 계획 컬럼"), ("freeze_exempt", "동결 기간 예외 컬럼"), ("ci_ids_json", "영향받는 CI 목록 컬럼"), ("EMERGENCY", "긴급 변경 타입"), ("APPROVE", "CAB 승인 투표"), ] for sym, desc in model_checks: status = "OK" if sym in models_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 3. routers/change.py 엔드포인트 확인 ===") with open("routers/change.py", encoding="utf-8") as f: router_src = f.read() endpoint_checks = [ ('@router.post("/rfc"', "POST /api/change/rfc"), ('@router.get("/rfc"', "GET /api/change/rfc"), ('@router.get("/rfc/{rfc_id}"', "GET /api/change/rfc/{id}"), ('@router.patch("/rfc/{rfc_id}"', "PATCH RFC"), ('@router.post("/rfc/{rfc_id}/submit"', "제출 (DRAFT→SUBMITTED)"), ('@router.post("/rfc/{rfc_id}/vote"', "CAB 투표"), ('@router.post("/rfc/{rfc_id}/decide"', "최종 결정"), ('@router.post("/rfc/{rfc_id}/schedule"', "일정 확정"), ('@router.post("/rfc/{rfc_id}/start"', "변경 시작"), ('@router.post("/rfc/{rfc_id}/complete"', "변경 완료"), ('@router.post("/rfc/{rfc_id}/fail"', "변경 실패"), ('@router.get("/rfc/{rfc_id}/votes"', "CAB 투표 현황"), ('@router.post("/freeze"', "동결 기간 등록"), ('@router.get("/freeze"', "동결 기간 목록"), ('@router.delete("/freeze/{freeze_id}"', "동결 기간 삭제"), ('@router.get("/freeze/check"', "동결 기간 확인"), ('@router.get("/calendar"', "변경 일정 캘린더"), ('@router.get("/stats"', "변경 통계"), ("_next_rfc_id", "RFC ID 생성 함수"), ("_check_freeze", "동결 기간 충돌 검사"), ("_count_votes", "투표 집계 함수"), ] for sym, desc in endpoint_checks: status = "OK" if sym in router_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 4. 상태 전환 흐름 검증 ===") # RFC 상태 흐름: DRAFT→SUBMITTED→IN_REVIEW→(APPROVED|REJECTED)→SCHEDULED→IN_PROGRESS→(COMPLETED|FAILED) state_flow = { "DRAFT": "초안", "SUBMITTED": "CAB 검토 제출", "IN_REVIEW": "검토 중", "APPROVED": "승인", "REJECTED": "거부", "SCHEDULED": "일정 확정", "IN_PROGRESS": "진행 중", "COMPLETED": "완료", "FAILED": "실패", "WITHDRAWN": "철회", } try: import importlib.util spec = importlib.util.spec_from_file_location("models_mod", "models.py") models_mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(models_mod) rfc_statuses = set(e.value for e in models_mod.RFCStatus) for st, desc in state_flow.items(): status = "OK" if st in rfc_statuses else "ERR" if status == "ERR": ok = False print(f" {status} RFCStatus.{st}: {desc}") except Exception as e: print(f" ERR Enum 로드 오류: {type(e).__name__}: {e}") ok = False print("\n=== 5. ChangeType / Risk / Vote Enum 검증 ===") try: change_types = set(e.value for e in models_mod.ChangeType) for t in ["STANDARD", "NORMAL", "EMERGENCY", "MAJOR"]: status = "OK" if t in change_types else "ERR" if status == "ERR": ok = False print(f" {status} ChangeType.{t}") risk_levels = set(e.value for e in models_mod.ChangeRisk) for r in ["LOW", "MEDIUM", "HIGH", "CRITICAL"]: status = "OK" if r in risk_levels else "ERR" if status == "ERR": ok = False print(f" {status} ChangeRisk.{r}") vote_results = set(e.value for e in models_mod.CABVoteResult) for v in ["APPROVE", "REJECT", "ABSTAIN", "DEFER"]: status = "OK" if v in vote_results else "ERR" if status == "ERR": ok = False print(f" {status} CABVoteResult.{v}") except Exception as e: print(f" ERR {type(e).__name__}: {e}") ok = False print("\n=== 6. 변경 관리 비즈니스 규칙 검증 ===") # 비즈니스 규칙이 코드에 구현되어 있는지 확인 rules = [ ("rollback_plan", "롤백 계획 필수 체크 (submit 시)"), ("change_plan", "변경 계획 필수 체크 (submit 시)"), ("freeze_exempt", "동결 기간 예외 처리"), ("_check_freeze", "동결 기간 충돌 검사 (schedule 시)"), ("is_final", "최종 결정권자 투표"), ("approval_rate", "승인율 계산"), ("success_rate", "변경 성공률 계산"), ("UserRole.ADMIN", "ADMIN 권한 검사 (decide)"), ("UserRole.PM", "PM 권한 검사 (decide)"), ] for sym, desc in rules: status = "OK" if sym in router_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 7. RFC ID 형식 검증 ===") from datetime import datetime today = datetime.utcnow().strftime("%Y%m%d") rfc_example = f"RFC-{today}-0001" # RFC-YYYYMMDD-NNNN: 4+8+1+4 = 17자 try: assert rfc_example.startswith("RFC-"), "RFC ID 형식 오류" assert len(rfc_example) == 17, f"RFC ID 길이 오류: {len(rfc_example)}" print(f" OK RFC ID 형식: {rfc_example} ({len(rfc_example)}자)") except AssertionError as e: print(f" ERR {e}") ok = False print("\n=== 8. main.py 등록 확인 ===") for sym, desc in [("change", "change 임포트"), ("change.router", "change 라우터 등록")]: status = "OK" if sym in open("main.py", encoding="utf-8").read() else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== C-2 변경 관리 CAB 테스트 완료 ===") if ok: print("모든 검사 통과") else: sys.exit(1)