"""549-2 시스템 관리 · 내 정보 세부 페이지 시험 — 화면 조작은 ORCA. · 거르기 · 게이지 색 · 로그 글 = 순수 함수를 Node 로 바로 돌림 · 여섯 열쇠가 새 페이지로 갈아 끼워짐 · 시스템 관리 페이지는 시스템 관리자만(등록부 + 서버 둘 다) """ from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] B01 = ROOT / "B01_Dashboard" def _text(path: Path) -> str: return path.read_text(encoding="utf-8") def _slice(name: str, start: str, end: str) -> str: src = _text(B01 / name) return src[src.index(start) : src.index(end)] def _node(script: str) -> object: result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", script], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) needs_node = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") @needs_node def test_게이지_색_선(): src = _slice( "B01_Dashboard_UI_View_SysOverview.ts", "export const GAUGE_WARN", "function statCard" ) out = _node( src + "console.log(JSON.stringify([null, 0, 69.9, 70, 89.9, 90, 100].map(gaugeTone)));" ) assert out == ["ok", "ok", "ok", "warn", "warn", "bad", "bad"] @needs_node def test_사용자_거르기(): src = _slice("B01_Dashboard_UI_View_SysUsers.ts", "/** 역할 · 찾기", "const COLUMNS") users = [ {"id": 1, "name": "홍길동", "email": "hong@a.kr", "company_name": "예시건설", "role": "ADMIN"}, {"id": 2, "name": "김영희", "email": "kim@b.kr", "company_name": None, "role": "USER"}, {"id": 3, "name": "관리", "email": "sys@c.kr", "department": "운영", "role": "SYSTEM_ADMIN"}, ] # fmt: skip out = _node( "type DashboardUser = any;\n" + src + f"const u = {json.dumps(users, ensure_ascii=False)};" + "console.log(JSON.stringify([" "filterUsers(u, '', '').map(x => x.id)," "filterUsers(u, 'USER', '').map(x => x.id)," "filterUsers(u, '', '예시').map(x => x.id)," "filterUsers(u, '', 'KIM@B').map(x => x.id)," "filterUsers(u, '', 'null').map(x => x.id)," "filterUsers(u, 'ADMIN', '운영').map(x => x.id)]));" ) assert out == [[1, 2, 3], [2], [1], [2], [], []] @needs_node def test_로그_거르기_글(): src = _slice("B01_Dashboard_UI_View_SysLogs.ts", "export const ACTION_KO", "function dateInput") logs = [ {"action": "PROJECT_CREATE", "email": "a@x.kr", "resource_type": "project", "resource_ref": "12345678-aaaa-bbbb-cccc-1234567890ab", "ip_address": "1.2.3.4", "timestamp": "2026-10-20T09:00:00"}, {"action": "COMPANY_CREATE", "email": "b@x.kr", "resource_type": "COMPANY", "resource_id": 7, "ip_address": None, "timestamp": "2026-10-25T10:00:00"}, {"action": "NEW_THING", "email": None, "timestamp": "2026-10-26T11:00:00"}, ] # fmt: skip out = _node( "type AuditLog = any;\n" + "const dateText = (v: string | null | undefined, e: string) => (v ? v.slice(0, 10) : e);\n" + src + f"const l = {json.dumps(logs, ensure_ascii=False)};" + "const f = (o: object) => filterLogs(l, { action: '', from: '', to: '', q: '', ...o }).length;" + "console.log(JSON.stringify([" "actionLabel('PROJECT_CREATE'), actionLabel('NEW_THING')," "targetText(l[0]), targetText(l[1]), targetText(l[2])," "f({}), f({ action: 'COMPANY_CREATE' })," "f({ from: '2026-10-25' }), f({ to: '2026-10-20' }), f({ from: '2026-10-25', to: '2026-10-25' })," "f({ q: '프로젝트 생성' }), f({ q: '1.2.3' })]));" ) assert out == [ "프로젝트 생성", "NEW_THING", "프로젝트 12345678…", "회사 7", "-", 3, 1, 2, 1, 1, 1, 1, ] # fmt: skip def test_여섯_열쇠_새_페이지(): views = _text(B01 / "B01_Dashboard_UI_Views.ts") want = { "sys-overview": "mountSysOverview", "sys-companies": "mountSysCompanies", "sys-users": "mountSysUsers", "sys-defaults": "mountSysDefaults", "sys-logs": "mountSysLogs", "me": "mountMe", } # fmt: skip for key, mount in want.items(): line = next(x for x in views.splitlines() if f'key: "{key}"' in x) assert f"mount: {mount} " in line + " ", key if key != "me": assert 'roles: ["SYSTEM_ADMIN"]' in line, key # 시스템 관리 = 시스템 관리자만 legacy = _text(B01 / "B01_Dashboard_UI_Legacy.ts") for old in ("sysOverview", "sysCompanies", "sysUsers", "sysDefaults", "sysLogs", " me: (s)"): assert old not in legacy, old # 옛 구역 지움 def test_서버도_시스템_관리자만(): router = _text(B01 / "B01_Dashboard_Router.py") for route in ("/admin/companies", "/admin/users", "/admin/audit-logs", "/admin/resources"): block = re.search(r'@router\.get\("%s"\)[\s\S]*?\n\n\n' % re.escape(route), router) assert block and "Depends(require_system_admin)" in block.group(0), route def test_로그_쪽_불러오기_있음(): api = _text(B01 / "B01_Dashboard_Api_Fetch.ts") assert "export async function fetchAuditLogPage(" in api assert "/dashboard/admin/audit-logs?page=${page}&limit=${limit}" in api