package com.cloudhandson.vpdbackoffice.web; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import org.junit.jupiter.api.Test; class GuidedFlowTemplateTest { @Test void dashboardPrioritizesActionsAndKeepsDetailCollapsed() throws IOException { String html = template("dashboard.html"); String layout = template("fragments/layout.html"); String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js")); assertThat(html) .contains("th:replace=\"~{fragments/layout :: head('데이터 접근 제어')}\"") .contains("행 접근과 컬럼 마스킹 운영 흐름") .contains("업무 흐름") .contains("dashboard-menu-flow") .contains("도움말: 메뉴와 권한 적용 구조 보기") .contains("CB_APP_USER") .contains("CB_ORDS") .contains("CB_AGENT_CTX") .contains("권한 데이터 모델") .contains("접근 검증") .contains("행 접근 규칙") .contains("현재 상태") .contains("등록 대상 상세") .contains("
") .doesNotContain("journey-card") .doesNotContain("dashboard-workflow") .doesNotContain(">01<", ">02<"); assertThat(layout) .contains("운영 사용자 관리") .contains("접근 제어") .contains("보호·검증") .contains("연동 도구") .contains("운영") .contains("관리자") .contains("조회 연동") .contains("정형 데이터 조회") .contains("MCP 서비스") .contains("컬럼 마스킹") .contains("컬럼 원문 표시 허용") .contains("행 접근 필터 구조") .contains("보안 SQL 스크립트") .contains("고급 접근 조건") .contains("시스템 설정") .contains("DB 준비 상태") .contains("data-submenu-trigger") .contains("data-submenu-bar") .contains("data-submenu-panel") .contains("backoffice-can-mutate") .contains("backoffice-read-only") .doesNotContain("VPD Backoffice"); assertThat(javascript) .contains("data-submenu-trigger") .contains("data-submenu-panel") .contains("disableReadOnlyMutationForms") .contains("/vector-knowledge/search") .doesNotContain("closeAllMenuGroups"); } @Test void productPagesKeepBackgroundGuidanceBehindAConsistentHelpControl() throws IOException { for (String page : List.of( "permissions.html", "vpd-policies.html", "tokens.html", "probe.html", "objects.html", "effective-matrix.html", "users.html", "groups.html", "roles.html", "vector-knowledge.html", "operation-status.html", "settings.html", "settings-database.html", "ords-handlers.html", "vpd-filter-policies.html", "mcp-chatbot.html", "mcp-reasoning.html", "mcp-client-demo.html", "mcp-sse.html", "structured-data.html", "masking-rules.html", "user-masking-rules.html", "vpd-filter-runtime.html", "security-sql-scripts.html")) { assertThat(template(page)) .contains("도움말") .doesNotContain("context-summary") .doesNotContain("architectureStrip"); } assertThat(template("vector-knowledge.html")) .doesNotContain("1 · CONTENT", "2 · ACCESS POLICY", "3 · SEARCH"); } @Test void loginOffersRememberMeOnlyThroughTheSpringSecurityParameter() throws IOException { assertThat(template("login.html")) .contains("name=\"remember-me\"") .contains("데이터 접근 제어 콘솔") .contains("접근") .contains("로그인 유지") .contains("th:if=\"${rememberMeAvailable}\"") .doesNotContain("VPD"); } @Test void securitySqlScriptsCanRequestExplanationWithoutExecutingSql() throws IOException { String page = template("security-sql-scripts.html"); String result = template("fragments/security-sql-script-explanation.html"); assertThat(page) .contains("hx-post=\"/security-sql-scripts/explanation\"") .contains("LLM 전체·블록·토큰 처리 설명 생성") .contains("원문은 실행·수정하지 않고"); assertThat(result) .contains("LLM 스크립트 설명") .contains("토큰 처리와 행 접근/ASO 적용 흐름") .contains("data-markdown-view") .contains("근거 Prompt 보기"); } @Test void productFormsUseTaskNamesInsteadOfVisibleStepNumbers() throws IOException { String permissions = template("permissions.html"); String tokens = template("tokens.html"); String probe = template("probe.html"); String client = template("mcp-client-demo.html"); assertThat(permissions) .contains("data-wizard-target=\"1\">역할") .contains("data-wizard-target=\"2\">대상") .doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number"); assertThat(tokens) .contains("도구 사용자 토큰 발급", "검증 세션 발급") .doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급"); assertThat(probe) .contains("접근 검증 실행", "검증 결과") .doesNotContain("1. 발급받은 토큰 원문", "2. 확인할 데이터", "3. 접근 검증 실행"); assertThat(client) .contains("연결 시작", "도구 목록", "도구 호출") .doesNotContain("1", "2", "3"); } @Test void probeUsesOneTokenInputAndHidesTechnicalExchangeByDefault() throws IOException { String probe = template("probe.html"); String result = template("fragments/probe-result.html"); assertThat(probe) .contains("name=\"bearerToken\"") .contains("검증 세션 사용자") .contains("벡터 검색어 (평문)") .contains("임베딩 방식") .doesNotContain("벡터 검색 요청 본문 (JSON)") .doesNotContain("name=\"tokenKeyId\""); assertThat(result) .contains("적용된 사용자와 권한") .contains("DB 감사 실행 증적") .contains("DB가 감사한 실행 SQL") .contains("DB가 감사한 VPD predicate") .contains("참고용 SQL 재현") .contains("set_vpd_context 사용자 컨텍스트") .contains("CB_AGENT_CTX.USER_ID") .contains("벡터 유사도 검색 기준") .contains("역할 기반 권한 필터 (재현)") .contains("VECTOR_DISTANCE") .contains("벡터 검색 Top-K") .contains("벡터 거리 (SCORE)") .contains("vpd_predicate") .contains("다음에 할 일") .contains("담당 게임 사용자") .contains("value=\"STATIC_SQL\">정적 SQL 조건식") .contains("행 규칙의 두 가지 적용 방식 보기") .contains("한 권한 안의 규칙은 모두 AND") .contains("서로 다른 역할의 ALLOW 권한은 OR") .contains("data-wizard-validation") .contains("다른 조건 규칙은 한 권한에 함께 저장할 수 없습니다") .contains("저장 영향") .contains("되돌리기"); assertThat(javascript) .contains("validatePermissionWizardStep") .contains("ALL 규칙은 다른 조건 규칙과 함께 저장할 수 없습니다") .contains("movePermissionWizard"); } @Test void roleTokenAndVpdPagesExplainTheirBoundaries() throws IOException { String roles = template("roles.html"); String tokens = template("tokens.html"); String policies = template("vpd-policies.html"); String filters = template("vpd-filter-policies.html"); assertThat(roles) .contains("스마일게이트 Data & AI PoC 운영 역할") .contains("데이터 취급 등급"); assertThat(tokens) .contains("회수·만료 포함") .contains("현재 사용 가능한 토큰만"); assertThat(policies) .contains("어느 객체의 어떤 SQL에 어떤 Filter function을 붙일지") .contains("이 policy가 하는 일"); assertThat(filters) .contains("이 Filter가 보호하는 내용") .contains("기술 태그 비교 예시"); } @Test void connectionSettingsAreSeparatedFromDatabasePreparation() throws IOException { String settings = template("settings.html"); String database = template("settings-database.html"); assertThat(settings) .contains("시스템 설정") .contains("/settings/database") .doesNotContain("action=\"/settings/database/initialize\""); assertThat(database) .contains("DB 준비 상태") .contains("/settings/database/initialize") .contains("확인 문구 입력") .contains("DB 준비 실행") .doesNotContain("action=\"/settings/schema/initialize\""); } @Test void effectiveMatrixStartsWithOneUserAndKeepsGroupAndRoleAsSecondaryViews() throws IOException { String html = template("effective-matrix.html"); String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js")); assertThat(html) .contains("선택한 사용자의 최종 권한") .contains("data-effective-user-focus") .contains("data-effective-tab=\"user\"") .contains("data-effective-pane=\"group\"") .contains("data-effective-pane=\"role\""); assertThat(javascript) .contains("updateEffectiveUserFocus") .contains("initEffectiveMatrixTabs"); } @Test void mcpPagesDescribeReasoningAndProtocolBoundaries() throws IOException { String chatbot = template("mcp-chatbot.html"); String reasoning = template("mcp-reasoning.html"); String client = template("mcp-client-demo.html"); String sse = template("mcp-sse.html"); assertThat(chatbot).contains("검증 세션 사용자").contains("태그 벡터 검색"); assertThat(reasoning).contains("MCP tool을 고르고").contains("검증 세션 사용자").contains("MCP Tool"); assertThat(client).contains("tool 선택과 호출은 reasoning 결과").doesNotContain("Context Path"); assertThat(sse).contains("Instruction / parameter mapping").contains("Authorization"); } @Test void vectorKnowledgePageShowsIngestPermissionAndSearchFlow() throws IOException { String vector = template("vector-knowledge.html"); String result = template("fragments/vector-search-result.html"); assertThat(vector) .contains("지식자료 등록") .contains("ALLOW TAG SPRING_BOOT") .contains("DENY TAG ORDS") .contains("권한 기반 지식 검색") .contains("로컬 임베딩(개발용)") .contains("/vector-knowledge/ingest") .contains("/vector-knowledge/search"); assertThat(result) .contains("TECH_TAG") .contains("검색 기술 상세 보기"); } @Test void probeResultSeparatesDbExecutionEvidenceFromReconstructedSql() throws IOException { String result = template("fragments/probe-result.html"); String vectorSql = Files.readString(Path.of("sql/adb/29_agent_ords_vector_search_ords.sql")); assertThat(result) .contains("DB 감사 실행 증적") .contains("DB가 감사한 실행 SQL") .contains("DB가 감사한 VPD predicate") .contains("RLS_INFO") .contains("참고용 SQL 재현") .contains("SQL (재현)") .contains("행 접근 조건 (VPD predicate 재현)") .contains("ALL: 추가 행 필터 없음") .contains("기본 whitelist 역할"); assertThat(vectorSql) .contains("VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE)") .contains("cb_ords_handler_pkg.set_vpd_context(:auth_header, :probe_id)") .contains("IF TRIM(v_vpd_predicate) <> '1 = 1' THEN") .contains("' AND (' || v_vpd_predicate || ')'"); } @Test void vectorDefaultRolesAreSeededAsWhitelistAllRules() throws IOException { String setup = Files.readString(Path.of("sql/adb/32_dds_vector_tag_setup.sql")); assertThat(setup) .contains("ensure_vector_all_permission(10)") .contains("ensure_vector_all_permission(20)") .contains("ensure_vector_all_permission(30)") .contains("UPPER(TRIM(rule_type)) = 'TAG'") .contains("NULL, 'ALL', NULL") .doesNotContain("SYS.ODCIVARCHAR2LIST('SPRING_BOOT', 'ORDS')"); } @Test void defaultVerificationUsersUseHumanNamesInsteadOfRoleLikeAgentNames() throws IOException { String seed = Files.readString(Path.of("sql/adb/17_agent_ords_security_local_vpd_setup.sql")); String users = template("users.html"); String result = template("fragments/probe-result.html"); assertThat(seed) .contains("(101, '이에이치알'") .contains("(102, '박파이넨스'") .contains("(103, '김어드민'"); assertThat(users).contains("이에이치알").doesNotContain(">agent_hr<"); assertThat(result).contains("김어드민").doesNotContain(">agent_all<"); } @Test void vectorOrdsHandlerReadsBodyStreamOnlyOnce() throws IOException { String sql = Files.readString(Path.of("sql/adb/29_agent_ords_vector_search_ords.sql")); assertThat(sql) .contains("v_body_text CLOB") .contains("v_body_text := :body_text") .contains("Duplicate stream parameter") .doesNotContain("JSON_QUERY(\n :body_text") .doesNotContain("JSON_VALUE(\n :body_text"); assertThat(sql.split("v_body_text := :body_text", -1)).hasSize(2); assertThat(sql).contains("vpd_predicate").contains("effective_sql"); } @Test void existingOrdsSetupCanReturnTheVpdPredicateTrace() throws IOException { String setup = Files.readString(Path.of("sql/adb/22_agent_ords_security_ords_handler_setup.sql")); String grant = Files.readString(Path.of("sql/adb/33_agent_ords_sql_trace_grant.sql")); assertThat(setup) .contains("admin.cb_agent_doc_vpd_filter('ADMIN', 'CB_V_SEARCH_DOCUMENTS')") .contains("APEX_JSON.WRITE('effective_sql', v_effective_sql)"); assertThat(grant).contains("GRANT EXECUTE ON cb_agent_doc_vpd_filter TO cb_ords"); } private String template(String relativePath) throws IOException { return Files.readString(Path.of("src/main/resources/templates").resolve(relativePath)); } }