- 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>
188 lines
7.0 KiB
Python
188 lines
7.0 KiB
Python
"""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)
|