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("Data & AI Backoffice") .contains("사용자") .contains("접근 그룹") .contains("역할") .contains("토큰") .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("product?.displayName()") .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=\"SELF\">본인 직원 행") .contains("value=\"STATIC_SQL\">정적 SQL 조건식") .contains("HMM_LEAVE_VPD_FILTER") .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("데이터 취급 등급"); 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("tools/list.inputSchema") .contains("환경 설정으로 승인한 MCP 도구") .doesNotContain( "resolve_hr_term", "search_hr_data", "search_hr_policy", "kb_select_ai_vpd", "KB_CLAIMS"); } @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)); } }