"""B01 대시보드 「시스템 설정」 컨테이너 시험(27-3) — 화면 조작은 ORCA. · 역할별 카드(제목 · 설명 · 이동 해시)를 Node 로 바로 돌림 — 가짜 DOM · 모든 역할 한 줄 · 통째로 접힘 · 「구조물 관리」 컨테이너 없음 · 회사 고르기(29-1) — 시스템 관리자만 · 리소스 현황 아래 · 해시 company · 서버 인자 한 곳 """ from __future__ import annotations import json import re import shutil import subprocess from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] PAGE = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Page.ts" def _text(path: Path) -> str: return path.read_text(encoding="utf-8") def _fn(src: str, name: str) -> str: found = re.search(rf"^function {name}\([\s\S]*?^}}", src, re.M) assert found, f"{name} 를 못 찾음" return found.group(0) def _cards(user: dict, company_id: int | None = None) -> list: src = _text(PAGE) script = ( "interface DashboardUser { role: string; is_master: boolean }\n" "const ROUTES = { M01_MASTER_DATA: 'm01-master-data'," " M02_MASTER_TEMPLATE: 'm02-master-template' };\n" "const L = (k: string) => k;\n" "let went = '';\n" "const navigateTo = (r: string, q?: Record) =>" " { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n" "const mk = (): any => ({ children: [] as any[], textContent: '', className: ''," " on: null as any," " append(...c: any[]) { this.children.push(...c); }," " addEventListener(_: string, f: any) { this.on = f; } });\n" "const document = { createElement: () => mk() };\n" + _fn(src, "settingCard") + "\n" + _fn(src, "buildSystemSettingsPanel") + "\nconst user = JSON.parse(process.argv[1]);" + "\nconst panel = buildSystemSettingsPanel(user, JSON.parse(process.argv[2]));" + "\nconsole.log(JSON.stringify(panel.children.map((c: any) => {" + " c.on(); return [c.children[0].textContent, c.children[1].textContent," + " decodeURIComponent(went)]; })));" ) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)], capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert result.returncode == 0, result.stderr return json.loads(result.stdout) M02 = "m02-master-template" READ_CO = ( "B01_Dashboard_PermMasterRead · B01_Dashboard_PermCompanyRead · B01_Dashboard_PermPersonalWrite" ) WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite") @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_시스템_관리자_카드_다섯_고른_회사_해시(): m = "B01_Dashboard_PermMasterWrite" assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [ ["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"], ["B01_Dashboard_CardUnitCombo", m, f"{M02}?open=일위대가&root=master&company=3"], ["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"], ["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"], ["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"], ] # 회사가 없으면 해시에 안 실음 assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == ( f"{M02}?open=일위대가&root=master" ) @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_회사_관리자_개인_카드_둘(): company = [ ["B01_Dashboard_CardUnitCombo", WRITE_CO, f"{M02}?open=일위대가&root=company"], ["B01_Dashboard_CardStructCombo", WRITE_CO, f"{M02}?open=구조물조합&root=company"], ] assert _cards({"role": "ADMIN", "is_master": False}, 3) == company # 회사 인자 안 실음 assert _cards({"role": "USER", "is_master": True}) == company # 회사 마스터 = 회사 수정 assert _cards({"role": "USER", "is_master": False}) == [ ["B01_Dashboard_CardUnitCombo", READ_CO, f"{M02}?open=일위대가&root=personal"], ["B01_Dashboard_CardStructCombo", READ_CO, f"{M02}?open=구조물조합&root=personal"], ] def test_모든_역할_한_줄_통째로_접힘(): src = _text(PAGE) build = _fn(src, "buildPage") # 세 갈래 모두 한 번씩 · 시스템 관리자는 리소스 현황 아래(29-1) · 프로젝트 위 assert build.count("buildSystemSettingsSection(state)") == 3 sys_part = build.split('} else if (state.user.role === "ADMIN")')[0] assert ( sys_part.index('L("B01_Dashboard_Resources")') < sys_part.index("buildSystemSettingsSection(state)") < sys_part.index('L("B01_Dashboard_Projects")') ) assert "StructureManage" not in src and "buildStructurePanel" not in src assert "top-row" not in src sect = _fn(src, "buildSystemSettingsSection") assert 'L("B01_Dashboard_SystemSettings"), panel, true)' in sect # 한 줄 통째 assert '"ui-collapsible"' in sect and '"ui-collapsible__title"' in sect assert "attachCollapsible(card)" in sect config = _text(ROOT / "config" / "config_frontend.ts") assert re.search(r"PROTECTED_ROUTES[\s\S]*?ROUTES\.M02_MASTER_TEMPLATE,[\s\S]*?\];", config) def test_회사_알약_시스템_관리자만(): src = _text(PAGE) sect = _fn(src, "buildSystemSettingsSection") assert 'if (user.role === "SYSTEM_ADMIN") withCompanyPill(card, state);' in sect pill = _fn(src, "withCompanyPill") assert "sessionStorage.setItem(ADMIN_COMPANY_KEY, String(company.id))" in pill assert "event.stopPropagation()" in pill # 알약 누름이 접힘으로 새지 않음 assert "openCompanyPicker(state.allCompanies" in pill # 조회 후 고르기(29-5) assert '"ui-tag ui-tag--accent b01-dashboard__company-pill"' in pill # 공용 알약 모양 assert '.querySelector(".b01-dashboard__section-header > .ui-card__title")?.after(pill)' in pill assert 'const ADMIN_COMPANY_KEY = "aislo.adminCompany";' in src # M02 와 같은 키 load = src.split("async function loadRoleData")[1].split("\n}\n")[0] for call in ("fetchAllUsers(companyId)", "fetchCompanyJoinRequests(companyId)", "fetchCompanyProjects(companyId)"): # fmt: skip assert call in load, call build = _fn(src, "buildPage") # 회사 따라 바뀌는 컨테이너 넷(프로젝트 · 사용자 관리 · 가입 요청 · 회사 관리)마다 알약 sys_part = build.split('} else if (state.user.role === "ADMIN")')[0] assert sys_part.count("withCompanyPill(") == 4 assert "withCompanyPill" not in build.split('} else if (state.user.role === "ADMIN")')[1] assert "companyTable(picked, state.user)" in build assert "openAddMemberModal(undefined, state.adminCompanyId)" in build @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_고른_회사_저장값_검사(): src = _text(PAGE) script = ( "interface DashboardUser { company_id?: number | null }\n" "interface CompanyInfo { id: number }\n" "const ADMIN_COMPANY_KEY = 'aislo.adminCompany';\n" "const store: Record = {};\n" "const sessionStorage = { getItem: (k: string) => store[k] ?? null };\n" + _fn(src, "pickAdminCompany") + "\nconst cs = [{ id: 1 }, { id: 3 }];" + "\nconst out = [pickAdminCompany({ company_id: 1 }, cs)];" + "\nstore[ADMIN_COMPANY_KEY] = '3'; out.push(pickAdminCompany({ company_id: 1 }, cs));" + "\nstore[ADMIN_COMPANY_KEY] = '9'; out.push(pickAdminCompany({ company_id: 1 }, cs));" + "\nout.push(pickAdminCompany({ company_id: null }, cs));" + "\nconsole.log(JSON.stringify(out));" ) 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 # 기본 자기 회사 · 고른 회사 · 없는 회사면 자기 회사 · 자기 회사 없으면 첫 회사 assert json.loads(result.stdout) == [1, 3, 1, 1] def test_서버_회사_인자_한_곳(): scope = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Scope.py") assert 'if session["role"] == "SYSTEM_ADMIN" and company_id is not None:' in scope router = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_Router.py") for name in ( "admin_add_member", "admin_invite_member", "admin_join_requests", "admin_projects", ): body = router.split(f"async def {name}(")[1].split("@router.")[0] assert "company_id: int | None = Query(None, gt=0)" in body, name assert "_scope_company(session, company_id)" in body, name users = router.split("async def system_users(")[1].split("@router.")[0] assert "require_system_admin" in users and "list_all_users(company_id)" in users assert "router.include_router(assets_router)" in router assert len(router.splitlines()) <= 700 @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_회사_찾기_거르기(): """이름 · 사업자번호(줄표 빼고도) · 띄어쓰기 단어 모두 맞음(29-5).""" src = _text(ROOT / "B01_Dashboard" / "B01_Dashboard_UI_CompanyPick.ts") found = re.search(r"^export function filterCompanies[\s\S]*?^}", src, re.M) assert found, "filterCompanies 를 못 찾음" script = ( "interface CompanyInfo { id: number; name: string;" " business_registration_number?: string | null }\n" + found.group(0).replace("export ", "") + "\nconst cs = [{ id: 1, name: '캐미팩토리'," + " business_registration_number: '123-45-67890' }," + " { id: 3, name: '브레인검증회사', business_registration_number: null }," + " { id: 4, name: '캐미 건설', business_registration_number: '999-00-11111' }];" + "\nconst ids = (t: string) => filterCompanies(cs, t).map((c) => c.id);" + "\nconsole.log(JSON.stringify([ids(''), ids('캐미'), ids('캐미 건설'), ids('1234567890')," + " ids('123-45'), ids('브레인 없음'), ids(' ')]));" ) 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 assert json.loads(result.stdout) == [[1, 3, 4], [1, 4], [4], [1], [1], [], [1, 3, 4]] assert "const LIMIT = 50;" in src and "found.slice(0, LIMIT)" in src