- 개요 = 숫자 카드 넷 + CPU · 메모리 · 디스크 게이지(70 % 주황 · 90 % 빨강) + 7 일 SVG 그래프 - 회사 = 목록(찾기 · ⋯) → 상세(정보 · 사람 · 프로젝트 탭) · 사용자 = 회사 · 역할 · 찾기 거르기 · ⋯ - 설계 기본값 = 설정 카드를 묶음 제목 + 줄로 · 시스템 로그 = 동작 · 기간 · 찾기 거르기 + 이전 기록 더 보기 - 내 정보 = Linear 꼴 설정 줄(기본정보 · 비밀번호 · 테마 · 내 설계 기본값) - 옛 구역(리소스 패널 · 옛 내 정보 폼) 지움 · 지킴 시험 test_549 추가 · test_546 stub 맞춤 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
134 lines
5.3 KiB
Python
134 lines
5.3 KiB
Python
"""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"
|
|
+ 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
|