Files
vpd-permission-poc/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java

379 lines
16 KiB
Java

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("<details class=\"dashboard-details\">")
.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("<summary>도움말</summary>")
.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("<span class=\"login-mark\">접근</span>")
.contains("로그인 유지")
.contains("th:if=\"${rememberMeAvailable}\"")
.doesNotContain("<span class=\"login-mark\">VPD</span>");
}
@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("접근 검증 실행", "<strong>검증 결과</strong>")
.doesNotContain("1. 발급받은 토큰 원문", "2. 확인할 데이터", "3. 접근 검증 실행");
assertThat(client)
.contains("연결 시작", "도구 목록", "도구 호출")
.doesNotContain("<span>1</span>", "<span>2</span>", "<span>3</span>");
}
@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("<details")
.contains("기술 상세");
}
@Test
void vpdDefaultApplyAndAdvancedCustomFiltersAreVisuallySeparated() throws IOException {
String policies = template("vpd-policies.html");
String filters = template("vpd-filter-policies.html");
assertThat(policies)
.contains("action=\"/vpd-policies/default\"")
.contains("권한체계 자동 적용")
.contains("객체만 선택");
assertThat(filters)
.contains("기본 동작: 권한체계 자동 반영")
.contains("고급: 별도 Filter 만들기")
.contains("fail-closed")
.contains("CB_AGENT_DOC_VPD_FILTER");
}
@Test
void ordsObjectPageSeparatesAccessFromColumnDisplayProtection() throws IOException {
String html = template("objects.html");
assertThat(html)
.contains("누가 어떤 행을 볼 수 있는지는 행 접근 규칙과 Oracle VPD 정책에서 결정")
.contains("지식 검색 연결 흐름")
.contains("행 접근 context 설정과 기본 SELECT")
.contains("전용 벡터 Handler 사용")
.contains("컬럼 마스킹")
.doesNotContain("표시 보호 등급", "마스킹 방식");
}
@Test
void permissionWizardExplainsConditionCodeAndStaticSqlSemantics() throws IOException {
String html = template("permissions.html");
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
assertThat(html)
.contains("value=\"OWN_CUSTOMER\">담당 게임 사용자")
.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));
}
}