379 lines
16 KiB
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));
|
|
}
|
|
}
|