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 org.junit.jupiter.api.Test; class GuidedFlowTemplateTest { @Test void dashboardExplainsTheFourStepPermissionJourney() throws IOException { String html = template("dashboard.html"); assertThat(html) .contains("1. 권한 설계") .contains("2. DB 보호 연결") .contains("3. 검증 세션") .contains("4. 결과 확인"); } @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("토큰 적용 후 SQL") .contains("set_vpd_context 사용자 컨텍스트") .contains("CB_AGENT_CTX.USER_ID") .contains("벡터 검색 Top-K") .contains("벡터 거리 (SCORE)") .contains("vpd_predicate") .contains("다음에 할 일") .contains("특정 기술 태그") .contains("TECH_TAG") .contains("태그를 여러 개 추가하면"); } @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 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 probeResultExplainsVectorEffectiveSqlWhenTheHandlerReturnsTrace() 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("토큰 적용 후 SQL") .contains("권한 적용 SQL") .contains("VPD가 추가한 WHERE 조건"); assertThat(vectorSql) .contains("VECTOR_DISTANCE(d.embedding, TO_VECTOR(:embedding), COSINE)") .contains("WHERE d.embedding IS NOT NULL AND (' || v_vpd_predicate || ')"); } @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)); } }