refs #703 #704: finalize smilegate game data poc

This commit is contained in:
devmrko
2026-07-22 15:35:02 +09:00
parent 750bfbab5b
commit 3304b22bc4
42 changed files with 743 additions and 1095 deletions

View File

@@ -1,14 +1,8 @@
package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import com.cloudhandson.vpdbackoffice.config.CatalogProperties;
import com.cloudhandson.vpdbackoffice.config.MaskingProperties;
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
class MaskingPolicySynchronizerTest {
@@ -29,29 +23,8 @@ class MaskingPolicySynchronizerTest {
}
@Test
void hmmEmployeePolicyComesFromEnvironmentPolicyCatalog() {
ObjectMapper objectMapper = new ObjectMapper();
var dataCatalog = new EnvironmentDataCatalog(
new CatalogProperties("ADMIN", """
[{"key":"employees","tableName":"HMM_HR_EMPLOYEES","objectType":"TABLE",
"businessName":"직원","description":"직원"}]
"""),
objectMapper);
var policyCatalog = new EnvironmentMaskingPolicyCatalog(
new MaskingProperties("""
[{"objectName":"HMM_HR_EMPLOYEES","policyName":"HMM_EMPLOYEE_PII_REDACT"}]
"""),
objectMapper);
var synchronizer = new MaskingPolicySynchronizer(
mock(JdbcTemplate.class),
mock(MaskingRuleMapper.class),
dataCatalog,
policyCatalog);
assertThat(synchronizer.owner()).isEqualTo("ADMIN");
assertThat(synchronizer.managedPolicyName("HMM_HR_EMPLOYEES"))
.isEqualTo("HMM_EMPLOYEE_PII_REDACT");
assertThat(synchronizer.managedObjectNames())
.doesNotContain("KB_CUSTOMERS", "KB_CLAIMS", "KB_CONTRACTS");
void smilegateSalesTransactionPolicyIsManagedByBackoffice() {
assertThat(MaskingPolicySynchronizer.managedPolicyName("COMN_SALES_TXN"))
.isEqualTo("SG_SALES_TXN_REDACT");
}
}

View File

@@ -1,208 +1,71 @@
package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.McpProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
class McpSseServiceTest {
private static final String HMM_TOOLS = """
[
{
"name":"resolve_hr_term",
"label":"HMM HR 용어 표준화",
"description":"휴가·근태 표현을 표준 용어와 코드로 변환합니다.",
"argumentName":"term",
"argumentDescription":"확인할 휴가·근태 용어입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_TERM_RESOLVER",
"targetParameterName":"P_TERM"
},
{
"name":"search_hr_data",
"label":"HMM HR 데이터 조회",
"description":"조직, 직원, 휴가, 근태 데이터를 조회합니다.",
"argumentName":"query",
"argumentDescription":"완전한 자연어 질문입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_NORMALIZED_DATA_SEARCH",
"targetParameterName":"P_QUERY"
},
{
"name":"search_hr_policy",
"label":"HMM HR 규정 검색",
"description":"HR 규정 PDF를 검색합니다.",
"argumentName":"query",
"argumentDescription":"정책에 대한 자연어 질문입니다.",
"executionType":"AGENT_TOOL",
"targetName":"HMM_HR_POLICY_SEARCH",
"targetParameterName":"P_QUERY"
}
]
""";
private final ObjectMapper objectMapper = new ObjectMapper();
private final McpProperties mcpProperties =
new McpProperties(
"https://example.com/mcp",
"hmm-hr-backoffice",
"",
"",
"",
"",
HMM_TOOLS);
private final McpToolCatalog toolCatalog =
new EnvironmentMcpToolCatalog(mcpProperties, objectMapper);
private final CapturingHmmAiAgentToolRunner agentToolRunner =
new CapturingHmmAiAgentToolRunner();
private final HmmMcpBearerAuthenticator bearerAuthenticator =
token -> new HmmMcpPrincipal(1L, "E1001", 1L);
private final SmilegateSelectAiService selectAiService = new CapturingSmilegateSelectAiService();
private final McpSseService service = new McpSseService(
agentToolRunner,
mock(SelectAiService.class),
bearerAuthenticator,
toolCatalog,
mcpProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
selectAiService,
objectMapper
);
@Test
void listsEnvironmentConfiguredTermDataAndPolicyTools() {
ObjectNode response =
service.handle("default", request(1, "tools/list"), "valid-token");
void listsOnlyVpdSelectAiToolWithPromptInput() {
ObjectNode response = service.handle("default", request(1, "tools/list"));
var tools = response.path("result").path("tools");
assertThat(tools).hasSize(3);
assertThat(tools).extracting(node -> node.path("name").asText())
.containsExactly("resolve_hr_term", "search_hr_data", "search_hr_policy");
assertThat(tools.get(0).path("inputSchema").path("required"))
.extracting(JsonNode::asText)
.containsExactly("term");
assertThat(tools.get(1).path("inputSchema").path("required"))
.extracting(JsonNode::asText)
.containsExactly("query");
assertThat(tools).hasSize(1);
var selectAi = tools.get(0);
assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.smilegate_game_text2sql");
assertThat(selectAi.path("inputSchema").path("required"))
.extracting(node -> node.asText())
.contains("prompt");
assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse();
assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isFalse();
assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();
}
@Test
void callsConfiguredAgentToolWithItsDeclaredInputName() {
void callsVpdSelectAiThroughOrdsService() {
ObjectNode request = request(2, "tools/call");
ObjectNode params = request.putObject("params");
params.put("name", "resolve_hr_term");
params.putObject("arguments").put("term", "연차 이월");
ObjectNode params = (ObjectNode) request.putObject("params");
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
ObjectNode arguments = params.putObject("arguments");
arguments.put("prompt", "카제나 AU를 조회해 줘");
ObjectNode response = service.handle("default", request, "valid-token");
ObjectNode response = service.handle("default", request, "user-bearer");
assertThat(agentToolRunner.toolName).isEqualTo("HMM_HR_TERM_RESOLVER");
assertThat(agentToolRunner.input.path("P_TERM").asText()).isEqualTo("연차 이월");
assertThat(agentToolRunner.bearerToken).isEqualTo("valid-token");
CapturingSmilegateSelectAiService agentService =
(CapturingSmilegateSelectAiService) selectAiService;
assertThat(agentService.bearerToken).isEqualTo("user-bearer");
assertThat(agentService.prompt).isEqualTo("카제나 AU를 조회해 줘");
assertThat(response.path("error").isMissingNode()).isTrue();
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
}
@Test
void routesSelectAiToolThroughTheVpdAwareExecutor() {
String selectAiTools = """
[
{
"name":"search_hr_data",
"label":"HMM HR 데이터 조회",
"description":"VPD가 적용된 HMM HR 데이터를 조회합니다.",
"argumentName":"query",
"argumentDescription":"완전한 자연어 질문입니다.",
"executionType":"SELECT_AI"
}
]
""";
McpProperties selectAiProperties =
new McpProperties("", "", "", "", "", "", selectAiTools);
SelectAiService selectAiService = mock(SelectAiService.class);
when(selectAiService.generateAndExecute(
"valid-token", "내 휴가 신청 내역을 보여줘"))
.thenReturn(objectMapper.createObjectNode().put("vpdEnforced", true));
McpSseService selectAiMcp = new McpSseService(
agentToolRunner,
selectAiService,
bearerAuthenticator,
new EnvironmentMcpToolCatalog(selectAiProperties, objectMapper),
selectAiProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
ObjectNode request = request(5, "tools/call");
request.putObject("params")
.put("name", "search_hr_data")
.putObject("arguments")
.put("query", "내 휴가 신청 내역을 보여줘");
ObjectNode response =
selectAiMcp.handle("default", request, "valid-token");
verify(selectAiService).generateAndExecute(
"valid-token", "내 휴가 신청 내역을 보여줘");
assertThat(response.path("result").path("isError").asBoolean()).isFalse();
assertThat(response.path("result").path("content").get(0).path("text").asText())
.contains("\"vpdEnforced\" : true");
.contains("SGMP_POC_HAIKU45")
.contains("SELECT 1 FROM DUAL");
}
@Test
void rejectsDiscoveryWhenBearerAuthenticationFails() {
McpSseService rejectingService = new McpSseService(
agentToolRunner,
mock(SelectAiService.class),
token -> {
throw new McpUnauthorizedException();
},
toolCatalog,
mcpProperties,
new BackofficeProperties(null, null, null, null, null),
objectMapper);
assertThatThrownBy(() ->
rejectingService.handle("default", request(4, "tools/list"), "invalid-token"))
.isInstanceOf(McpUnauthorizedException.class);
}
@Test
void rejectsUnknownToolsWithoutCallingTheRunner() {
void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
ObjectNode request = request(3, "tools/call");
ObjectNode params = request.putObject("params");
params.put("name", "unconfigured.tool");
params.putObject("arguments").put("prompt", "query");
ObjectNode params = (ObjectNode) request.putObject("params");
params.put("name", "oracle.select_ai.smilegate_game_text2sql");
params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");
ObjectNode response = service.handle("default", request, "valid-token");
ObjectNode response = service.handle("default", request, "");
assertThat(response.path("result").isMissingNode()).isTrue();
assertThat(response.path("error").path("message").asText())
.contains("등록되지 않은 MCP tool");
}
@Test
void rejectsDuplicateToolNamesAtStartup() {
String duplicate = """
[
{"name":"same","label":"A","description":"A","argumentName":"query",
"argumentDescription":"A","executionType":"AGENT_TOOL",
"targetName":"TOOL_A","targetParameterName":"P_QUERY"},
{"name":"same","label":"B","description":"B","argumentName":"query",
"argumentDescription":"B","executionType":"AGENT_TOOL",
"targetName":"TOOL_B","targetParameterName":"P_QUERY"}
]
""";
McpProperties duplicateProperties =
new McpProperties("", "", "", "", "", "", duplicate);
assertThatThrownBy(() ->
new EnvironmentMcpToolCatalog(duplicateProperties, objectMapper))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("BACKOFFICE_MCP_TOOLS");
assertThat(response.path("error").isMissingNode()).isTrue();
assertThat(response.path("result").path("isError").asBoolean()).isTrue();
assertThat(response.path("result").path("content").get(0).path("text").asText())
.contains("VPD_TOKEN_DENIED")
.contains("권한이 없습니다");
}
private ObjectNode request(int id, String method) {
@@ -213,23 +76,22 @@ class McpSseServiceTest {
return request;
}
private final class CapturingHmmAiAgentToolRunner
implements HmmAiAgentToolRunner {
private static final class CapturingSmilegateSelectAiService extends SmilegateSelectAiService {
private String toolName;
private ObjectNode input;
private String bearerToken;
private String prompt;
private CapturingSmilegateSelectAiService() {
super(null, null, null, new ObjectMapper());
}
@Override
public JsonNode run(
String requestedToolName,
ObjectNode requestedInput,
String bearerToken
) {
toolName = requestedToolName;
input = requestedInput.deepCopy();
public JsonNode generateShowSql(String bearerToken, String prompt) {
this.bearerToken = bearerToken;
return objectMapper.createObjectNode().put("status", "ok");
this.prompt = prompt;
return new ObjectMapper().createObjectNode()
.put("profile", "SGMP_POC_HAIKU45")
.put("generatedSql", "SELECT 1 FROM DUAL");
}
}
}

View File

@@ -3,51 +3,30 @@ package com.cloudhandson.vpdbackoffice.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import com.cloudhandson.vpdbackoffice.config.SecuritySqlScriptProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
class SecuritySqlScriptServiceTest {
private final SecuritySqlScriptService service = new SecuritySqlScriptService(
new SecuritySqlScriptProperties("""
[{
"scriptId":"hmm-leave-vpd",
"category":"HMM / VPD",
"fileName":"72_hmm_leave_team_vpd.sql",
"title":"HMM 휴가 팀 접근 정책",
"description":"팀장과 팀원 휴가 행 접근 정책"
}]
"""),
new ObjectMapper());
private final SecuritySqlScriptService service = new SecuritySqlScriptService();
@Test
void exposesOnlyEnvironmentAllowlistedBundledScripts() {
void exposesOnlyTheCuratedGitTrackedSecurityScripts() {
assertThat(service.list())
.extracting(item -> item.fileName())
.containsExactly("72_hmm_leave_team_vpd.sql");
.containsExactly(
"70_sg_tool_user.sql",
"71_sg_identity_administration.sql"
);
assertThat(service.find("hmm-leave-vpd").source())
.contains("HMM_ACCESS_CTX_PKG")
.contains("HMM_LEAVE_VPD_FILTER");
assertThat(service.find("smilegate-identity-administration").source())
.contains("create table sg_app_user")
.contains("DATA_AI_TF");
}
@Test
void rejectsUnknownScriptIdsInsteadOfResolvingARequestPath() {
void rejectsUnknownScriptIdsInsteadOfResolvingAPathFromRequestInput() {
assertThatThrownBy(() -> service.find("../../etc/passwd"))
.isInstanceOf(AppException.class)
.hasMessageContaining("조회할 수 없는");
}
@Test
void rejectsUnsafeResourcePathsAtStartup() {
assertThatThrownBy(() -> new SecuritySqlScriptService(
new SecuritySqlScriptProperties("""
[{"scriptId":"bad","category":"x","fileName":"../../etc/passwd.sql",
"title":"x","description":"x"}]
"""),
new ObjectMapper()))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("BACKOFFICE_SECURITY_SQL_SCRIPTS");
}
}

View File

@@ -4,72 +4,31 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import com.cloudhandson.vpdbackoffice.config.CatalogProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
class StructuredDataServiceTest {
private static final String HMM_OBJECTS = """
[
{"key":"teams","tableName":"HMM_ORG_TEAMS","objectType":"TABLE",
"businessName":"조직 원장","description":"조직 정보"},
{"key":"employees","tableName":"HMM_HR_EMPLOYEES","objectType":"TABLE",
"businessName":"직원 원장","description":"직원 정보"},
{"key":"leave-balances","tableName":"HMM_LEAVE_BALANCES","objectType":"VIEW",
"businessName":"휴가 잔여 원장","description":"휴가 잔여 정보"}
]
""";
private final DataCatalog catalog = new EnvironmentDataCatalog(
new CatalogProperties("ADMIN", HMM_OBJECTS), new ObjectMapper());
private final StructuredDataService service =
new StructuredDataService(mock(JdbcTemplate.class), catalog);
private final StructuredDataService service = new StructuredDataService(mock(JdbcTemplate.class));
@Test
void exposesOnlyEnvironmentConfiguredTablesAndViews() {
assertThat(service.owner()).isEqualTo("ADMIN");
void exposesOnlyTheSevenApprovedSmilegateStructuredTables() {
assertThat(service.tables())
.extracting(table -> table.tableName())
.containsExactly(
"HMM_ORG_TEAMS",
"HMM_HR_EMPLOYEES",
"HMM_LEAVE_BALANCES");
assertThat(service.requireTable("leave-balances").objectType()).isEqualTo("VIEW");
assertThat(service.previewSql(service.requireTable("employees")))
.isEqualTo("SELECT * FROM \"ADMIN\".\"HMM_HR_EMPLOYEES\" WHERE ROWNUM <= ?");
"CZN_COMN_USER_MST",
"CZN_COMN_CHARACTER_MST",
"COMN_SALES_TXN",
"COMN_REFUND_TXN",
"COMN_SALES_PRODUCT_DISP_BAS",
"COMN_GAME_SERVER_BAS",
"COMN_GAME_ALIAS_BAS");
}
@Test
void rejectsAnyObjectOutsideTheServerSideAllowlist() {
assertThatThrownBy(() -> service.requireTable("KB_SECURITY_AUDIT_LOG"))
void rejectsAnyTableOutsideTheServerSideAllowlist() {
assertThatThrownBy(() -> service.requireTable("security-audit-log"))
.isInstanceOf(AppException.class)
.hasMessage("선택할 수 없는 카탈로그 객체입니다.");
}
@Test
void rejectsUnsafeOracleIdentifiers() {
assertThatThrownBy(() -> new EnvironmentDataCatalog(
new CatalogProperties("ADMIN; DROP USER X", HMM_OBJECTS), new ObjectMapper()))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("BACKOFFICE_CATALOG_OWNER");
}
@Test
void rejectsDuplicateObjectNamesEvenWhenKeysDiffer() {
String duplicate = """
[
{"key":"employees","tableName":"HMM_HR_EMPLOYEES","objectType":"TABLE",
"businessName":"직원","description":"직원"},
{"key":"workers","tableName":"HMM_HR_EMPLOYEES","objectType":"VIEW",
"businessName":"직원 뷰","description":"직원 뷰"}
]
""";
assertThatThrownBy(() -> new EnvironmentDataCatalog(
new CatalogProperties("ADMIN", duplicate), new ObjectMapper()))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("BACKOFFICE_CATALOG_OBJECTS");
.hasMessage("선택할 수 없는 정형 데이터 테이블입니다.");
}
}

View File

@@ -35,11 +35,25 @@ class GuidedFlowTemplateTest {
.doesNotContain("dashboard-workflow")
.doesNotContain(">01<", ">02<");
assertThat(layout)
.contains("Data &amp; AI Backoffice")
.contains("사용자")
.contains("접근 그룹")
.contains("역할")
.contains("토큰")
.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");
@@ -108,7 +122,7 @@ class GuidedFlowTemplateTest {
.contains("data-wizard-target=\"2\">대상")
.doesNotContain(">1 역할<", ">2 객체<", "wizard-step-number");
assertThat(tokens)
.contains("직원 토큰 발급", "검증 세션 발급")
.contains("도구 사용자 토큰 발급", "검증 세션 발급")
.doesNotContain("1. 검증할 사용자 선택", "2. 검증 세션 발급");
assertThat(probe)
.contains("접근 검증 실행", "<strong>검증 결과</strong>")
@@ -184,8 +198,7 @@ class GuidedFlowTemplateTest {
String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js"));
assertThat(html)
.contains("value=\"MANAGED_TEAM\">본인 및 직접 보고 팀원")
.contains("value=\"SELF\">본인 직원 행")
.contains("value=\"OWN_CUSTOMER\">담당 게임 사용자")
.contains("value=\"STATIC_SQL\">정적 SQL 조건식")
.contains("HMM_LEAVE_VPD_FILTER")
.contains("행 규칙의 두 가지 적용 방식 보기")
@@ -209,6 +222,7 @@ class GuidedFlowTemplateTest {
String filters = template("vpd-filter-policies.html");
assertThat(roles)
.contains("스마일게이트 Data & AI PoC 운영 역할")
.contains("데이터 취급 등급");
assertThat(tokens)
.contains("회수·만료 포함")

View File

@@ -31,26 +31,26 @@ class MaskingRuleTemplateRenderTest {
var engine = new SpringTemplateEngine();
engine.setTemplateResolver(resolver);
var object = new ProtectedObject(7L, "POC_2", "KB_CUSTOMERS", "kb/customers", "Y");
var contractObject = new ProtectedObject(8L, "POC_2", "KB_CONTRACTS", "kb/contracts", "Y");
var column = new ProtectedColumn(11L, 7L, "RRN_MASKED", "Y", null, "RESTRICTED", "NULLIFY");
var rule = new MaskingRule(1L, "KB_RRN_STANDARD", "주민번호 기본 마스킹", "RRN_PARTIAL", "테스트", "Y");
var columnRule = new ColumnMaskingRule(11L, 7L, "POC_2", "KB_CUSTOMERS", "RRN_MASKED",
1L, "KB_RRN_STANDARD", "주민번호 기본 마스킹", "RRN_PARTIAL", "Y");
var object = new ProtectedObject(7L, "SGMP_POC", "CZN_COMN_USER_MST", "sgmp-poc/czn-users", "Y");
var transactionObject = new ProtectedObject(8L, "SGMP_POC", "COMN_SALES_TXN", "sgmp-poc/sales", "Y");
var column = new ProtectedColumn(11L, 7L, "GUID", "Y", null, "RESTRICTED", "NULLIFY");
var rule = new MaskingRule(1L, "GAME_GUID_MASK", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "테스트", "Y");
var columnRule = new ColumnMaskingRule(11L, 7L, "SGMP_POC", "CZN_COMN_USER_MST", "GUID",
1L, "GAME_GUID_MASK", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "Y");
var context = new Context(Locale.KOREAN);
context.setVariable("_csrf", new CsrfFixture("_csrf", "test-token"));
context.setVariable("templates", List.of(MaskingTemplate.values()));
context.setVariable("rules", List.of(rule));
context.setVariable("columnRules", List.of(columnRule));
var status = new MaskingPolicyStatus(
"POC_2", "KB_CUSTOMERS", "KB_CUSTOMER_PII_REDACT", "YES", 1, 1, 0, 0
"SGMP_POC", "CZN_COMN_USER_MST", "SG_CZN_USER_REDACT", "YES", 1, 1, 0, 0
);
context.setVariable("policyStatuses", List.of(status));
context.setVariable("policyStatusByObjectName", Map.of("KB_CUSTOMERS", status));
context.setVariable("objects", List.of(object, contractObject));
context.setVariable("maskingTargetObjects", List.of(object, contractObject));
context.setVariable("policyStatusByObjectName", Map.of("CZN_COMN_USER_MST", status));
context.setVariable("objects", List.of(object, transactionObject));
context.setVariable("maskingTargetObjects", List.of(object, transactionObject));
context.setVariable("sensitiveColumnsByObject", Map.of(7L, List.of(column), 8L, List.of()));
context.setVariable("availableMaskingColumnsByObject", Map.of(7L, List.of("CUST_NM"), 8L, List.of("PREMIUM")));
context.setVariable("availableMaskingColumnsByObject", Map.of(7L, List.of("GUID"), 8L, List.of("GUID")));
String rulesPage = engine.process("masking-rules", context);
@@ -64,13 +64,13 @@ class MaskingRuleTemplateRenderTest {
.contains("DB Redaction 컬럼")
.contains("레거시 VPD 컬럼 제어")
.contains("마스킹 대상 컬럼 추가")
.contains("POC_2.KB_CONTRACTS.PREMIUM")
.contains("KB_CUSTOMER_PII_REDACT")
.contains("SGMP_POC.COMN_SALES_TXN.GUID")
.contains("SG_CZN_USER_REDACT")
.contains("적용됨");
context.setVariable("users", List.of(new AppUser(2L, "KB_VPD_ADMIN", "ADMIN", "SEC", "N", "Y")));
context.setVariable("userRules", List.of(new UserMaskingRule(2L, "KB_VPD_ADMIN", 11L,
"POC_2", "KB_CUSTOMERS", "RRN_MASKED", "주민번호 기본 마스킹", "RRN_PARTIAL", "UNMASK", "Y")));
context.setVariable("users", List.of(new AppUser(2L, "sg-teamlead", "SG-001", "DATA_AI", "N", "Y")));
context.setVariable("userRules", List.of(new UserMaskingRule(2L, "sg-teamlead", 11L,
"SGMP_POC", "CZN_COMN_USER_MST", "GUID", "게임 사용자 식별자 기본 마스킹", "TEXT_PARTIAL", "UNMASK", "Y")));
String userPage = engine.process("user-masking-rules", context);
assertThat(userPage)