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('VPD 권한 운영')}\"") .contains("권한 관리") .contains("접근 검증") .contains("접근 규칙") .contains("현재 상태") .contains("상세 현황") .contains("
") .doesNotContain("journey-card") .doesNotContain("dashboard-workflow") .doesNotContain(">01<", ">02<"); assertThat(layout) .contains("VPD 권한 운영") .contains("권한 관리") .contains("보호·검증") .contains("연동 도구") .contains("운영·고급") .contains("조회 연동") .contains("MCP 서비스") .contains("고급 접근 조건") .contains("시스템 설정") .contains("data-submenu-trigger") .contains("data-submenu-bar") .contains("data-submenu-panel") .doesNotContain("VPD Backoffice"); assertThat(javascript) .contains("data-submenu-trigger") .contains("data-submenu-panel") .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")) { assertThat(template(page)) .contains("도움말") .doesNotContain("context-summary") .doesNotContain("architectureStrip"); } assertThat(template("vector-knowledge.html")) .doesNotContain("1 · CONTENT", "2 · ACCESS POLICY", "3 · SEARCH"); } @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("권한 조건 재현 SQL") .contains("set_vpd_context 사용자 컨텍스트") .contains("CB_AGENT_CTX.USER_ID") .contains("벡터 유사도 검색 기준") .contains("역할 기반 권한 필터 (VPD)") .contains("VECTOR_DISTANCE") .contains("벡터 검색 Top-K") .contains("벡터 거리 (SCORE)") .contains("vpd_predicate") .contains("다음에 할 일") .contains("특정 기술 태그") .contains("TECH_TAG") .contains("태그를 여러 개 추가하면") .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("표시 보호 등급 상한") .contains("행 접근 권한은 권한 규칙과 VPD에서 결정합니다"); 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("bearerToken"); } @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("DBMS_XPLAN Predicate Information") .contains("SQL_ID") .contains("권한 조건 재현 SQL") .contains("권한 적용 SQL") .contains("VPD가 추가한 WHERE 조건") .contains("ALL: 추가 행 필터 없음") .contains("기본 whitelist 역할"); assertThat(vectorSql) .contains("VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE)") .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)); } }