G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현 G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건) G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST) G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직 G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트 G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트 G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트 G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트 G-10: core/push_notify.py + routers/push.py + PushSubscription 모델 G-11: approvals 다중승인 (위임/서명/기한초과/마감연장) G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh 하네스: guardia-orchestrator 확장기능 Phase 반영 봇명령어: /sr /status /license /bulk 슬래시 명령어 추가 설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
8.2 KiB
Python
206 lines
8.2 KiB
Python
"""F-4 Mobile PWA 테스트"""
|
|
import sys, os, json, re, ast
|
|
|
|
ok = True
|
|
|
|
print("=== 1. 파일 존재 확인 ===")
|
|
pwa_files = [
|
|
"static/manifest.json",
|
|
"static/sw.js",
|
|
"static/offline.html",
|
|
]
|
|
for f in pwa_files:
|
|
exists = os.path.exists(f)
|
|
status = "OK" if exists else "ERR"
|
|
if not exists: ok = False
|
|
print(f" {status} {f}")
|
|
|
|
print("\n=== 2. manifest.json 필수 필드 검증 ===")
|
|
try:
|
|
with open("static/manifest.json", encoding="utf-8") as f:
|
|
manifest = json.load(f)
|
|
|
|
required_fields = ["name", "short_name", "start_url", "display", "icons",
|
|
"background_color", "theme_color", "lang"]
|
|
for field in required_fields:
|
|
status = "OK" if field in manifest else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {field}: {manifest.get(field, 'MISSING')!r}")
|
|
|
|
# display 값 검증
|
|
valid_displays = {"standalone", "fullscreen", "minimal-ui", "browser"}
|
|
assert manifest["display"] in valid_displays, \
|
|
f"display 값 오류: {manifest['display']}"
|
|
print(f" OK display='{manifest['display']}' (PWA 설치형)")
|
|
|
|
# 아이콘 최소 192x192, 512x512 필요
|
|
icon_sizes = {icon["sizes"] for icon in manifest.get("icons", [])}
|
|
assert "192x192" in icon_sizes, f"192x192 아이콘 없음. 있는 크기: {icon_sizes}"
|
|
assert "512x512" in icon_sizes, f"512x512 아이콘 없음. 있는 크기: {icon_sizes}"
|
|
print(f" OK 아이콘 {len(manifest['icons'])}개 (192, 512 포함)")
|
|
|
|
# shortcuts 존재 확인
|
|
shortcuts = manifest.get("shortcuts", [])
|
|
assert len(shortcuts) >= 2, f"shortcuts 최소 2개 필요: {len(shortcuts)}"
|
|
print(f" OK shortcuts {len(shortcuts)}개 정의됨")
|
|
|
|
# lang 한국어
|
|
assert manifest.get("lang") == "ko", f"lang='ko' 기대: {manifest.get('lang')}"
|
|
print(f" OK lang='ko' 한국어")
|
|
|
|
# start_url
|
|
assert manifest.get("start_url") == "/", f"start_url='/' 기대"
|
|
print(f" OK start_url='/'")
|
|
|
|
# scope
|
|
assert manifest.get("scope") == "/", f"scope='/' 기대"
|
|
print(f" OK scope='/'")
|
|
|
|
# maskable 아이콘
|
|
maskable = [i for i in manifest.get("icons", []) if "maskable" in i.get("purpose", "")]
|
|
assert len(maskable) > 0, "maskable 아이콘 없음"
|
|
print(f" OK maskable 아이콘 {len(maskable)}개")
|
|
|
|
except json.JSONDecodeError as e:
|
|
print(f" ERR manifest.json JSON 파싱 오류: {e}")
|
|
ok = False
|
|
except AssertionError as e:
|
|
print(f" ERR {e}")
|
|
ok = False
|
|
|
|
print("\n=== 3. sw.js Service Worker 핵심 기능 확인 ===")
|
|
with open("static/sw.js", encoding="utf-8") as f:
|
|
sw_src = f.read()
|
|
|
|
sw_checks = [
|
|
("CACHE_VERSION", "CACHE_VERSION 버전 관리"),
|
|
("STATIC_CACHE", "STATIC_CACHE 정적 캐시"),
|
|
("API_CACHE", "API_CACHE API 캐시"),
|
|
("OFFLINE_URL", "OFFLINE_URL 오프라인 폴백"),
|
|
("PRECACHE_URLS", "PRECACHE_URLS 사전 캐시 목록"),
|
|
("install", "install 이벤트 핸들러"),
|
|
("activate", "activate 이벤트 핸들러"),
|
|
("fetch", "fetch 이벤트 핸들러"),
|
|
("skipWaiting", "skipWaiting() 즉시 활성화"),
|
|
("clients.claim", "clients.claim() 클라이언트 제어"),
|
|
("caches.delete", "이전 캐시 자동 정리"),
|
|
("cacheFirst", "cacheFirst 전략"),
|
|
("networkFirst", "networkFirst 전략"),
|
|
("NO_CACHE_PATTERNS", "NO_CACHE_PATTERNS 보안 경로"),
|
|
("/api/auth", "auth 경로 캐시 제외"),
|
|
("/api/audit", "audit 경로 캐시 제외"),
|
|
("/api/pam", "pam 경로 캐시 제외"),
|
|
("push", "push 알림 핸들러"),
|
|
("notificationclick", "알림 클릭 핸들러"),
|
|
("sync", "백그라운드 동기화"),
|
|
("showNotification", "showNotification 알림 표시"),
|
|
("requireInteraction", "CRITICAL 알림 상호작용 필요"),
|
|
("503", "오프라인 503 응답"),
|
|
("offline: true", "offline 플래그 JSON 응답"),
|
|
("request.method !== 'GET'", "POST/PUT/DELETE 캐시 제외"),
|
|
]
|
|
for sym, desc in sw_checks:
|
|
status = "OK" if sym in sw_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 4. offline.html 구성 확인 ===")
|
|
with open("static/offline.html", encoding="utf-8") as f:
|
|
offline_src = f.read()
|
|
|
|
offline_checks = [
|
|
("<!DOCTYPE html>", "HTML5 DOCTYPE"),
|
|
('lang="ko"', "한국어 설정"),
|
|
("viewport", "뷰포트 메타"),
|
|
("theme-color", "theme-color 메타"),
|
|
("retryConnection", "retryConnection 재시도 함수"),
|
|
("/api/metrics/health", "헬스체크 엔드포인트 활용"),
|
|
("navigator.onLine", "온라인 상태 감지"),
|
|
("window.addEventListener('online'", "online 이벤트 리스너"),
|
|
("window.addEventListener('offline'","offline 이벤트 리스너"),
|
|
("location.reload", "자동 새로고침"),
|
|
("30000", "30초 자동 재시도"),
|
|
("history.back", "뒤로 가기"),
|
|
("animation", "오프라인 상태 애니메이션"),
|
|
]
|
|
for sym, desc in offline_checks:
|
|
status = "OK" if sym in offline_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 5. index.html PWA 태그 확인 ===")
|
|
with open("static/index.html", encoding="utf-8") as f:
|
|
index_src = f.read()
|
|
|
|
index_checks = [
|
|
('rel="manifest"', "manifest 링크"),
|
|
("/static/manifest.json", "manifest.json 경로"),
|
|
('name="theme-color"', "theme-color 메타"),
|
|
('name="mobile-web-app-capable"', "모바일 앱 가능"),
|
|
('name="apple-mobile-web-app-capable"', "iOS 앱 가능"),
|
|
('name="apple-mobile-web-app-title"', "iOS 앱 제목"),
|
|
('rel="apple-touch-icon"', "iOS 터치 아이콘"),
|
|
]
|
|
for sym, desc in index_checks:
|
|
status = "OK" if sym in index_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 6. app.js SW 등록 코드 확인 ===")
|
|
with open("static/app.js", encoding="utf-8") as f:
|
|
app_src = f.read()
|
|
|
|
app_checks = [
|
|
("serviceWorker", "serviceWorker 지원 감지"),
|
|
("register('/static/sw.js'", "SW 등록 경로"),
|
|
("scope: '/'", "SW 스코프 '/'"),
|
|
("updatefound", "updatefound 업데이트 감지"),
|
|
("installed", "installed 상태 감지"),
|
|
]
|
|
for sym, desc in app_checks:
|
|
status = "OK" if sym in app_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} {desc}")
|
|
|
|
print("\n=== 7. PWA 설치 기준 (Lighthouse) 충족 확인 ===")
|
|
try:
|
|
# 필수 조건 종합 검사
|
|
pwa_criteria = [
|
|
(manifest.get("name") and len(manifest["name"]) >= 2,
|
|
"앱 이름 2자 이상"),
|
|
(manifest.get("short_name") and len(manifest["short_name"]) <= 12,
|
|
"short_name 12자 이하"),
|
|
(manifest.get("start_url"),
|
|
"start_url 정의"),
|
|
(manifest.get("display") in {"standalone", "fullscreen", "minimal-ui"},
|
|
"display standalone/fullscreen/minimal-ui"),
|
|
(any(i["sizes"] == "192x192" for i in manifest.get("icons", [])),
|
|
"192x192 아이콘"),
|
|
(any(i["sizes"] == "512x512" for i in manifest.get("icons", [])),
|
|
"512x512 아이콘"),
|
|
("sw.js" in sw_src or "CACHE_VERSION" in sw_src,
|
|
"Service Worker 존재"),
|
|
(manifest.get("background_color"),
|
|
"background_color 정의"),
|
|
]
|
|
for check, desc in pwa_criteria:
|
|
status = "OK" if check else "ERR"
|
|
if not check: ok = False
|
|
print(f" {status} {desc}")
|
|
except Exception as e:
|
|
print(f" ERR {e}")
|
|
ok = False
|
|
|
|
print("\n=== 8. 보안 정책 확인 (캐시 제외 경로) ===")
|
|
no_cache_paths = ["/api/auth", "/api/audit", "/api/pam", "/api/otp", "/ws"]
|
|
for path in no_cache_paths:
|
|
status = "OK" if path in sw_src else "ERR"
|
|
if status == "ERR": ok = False
|
|
print(f" {status} '{path}' 캐시 제외 (보안)")
|
|
|
|
print("\n=== F-4 Mobile PWA 테스트 완료 ===")
|
|
if ok:
|
|
print("모든 검사 통과")
|
|
else:
|
|
sys.exit(1)
|