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.domain.audit.AuditEvent; import com.cloudhandson.vpdbackoffice.domain.permission.AppRole; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView; import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.mapper.AuditMapper; import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; class PermissionServiceTest { private PermissionMapper permissionMapper; private ProtectedObjectService protectedObjectService; private PermissionService permissionService; @BeforeEach void setUp() { permissionMapper = new FakePermissionMapper(); AuditService auditService = new AuditService(new NoopAuditMapper()); protectedObjectService = new ProtectedObjectService(null, auditService) { @Override public ProtectedObject assertEnabled(long objectId) { return new ProtectedObject(1L, "ADMIN", "CB_V_SEARCH_DOCUMENTS", "cb-agent-security/vpd/documents", "Y"); } @Override public List findColumns(long objectId) { return List.of( new ProtectedColumn(1L, 1L, "DEPT_CODE", "N", null, "INTERNAL", "NONE"), new ProtectedColumn(2L, 1L, "OWNER_EMP_NO", "N", null, "INTERNAL", "NONE"), new ProtectedColumn(3L, 1L, "TECH_TAG", "N", null, "PUBLIC", "NONE") ); } }; permissionService = new PermissionService(permissionMapper, protectedObjectService, auditService); } @Test void rejectsAllRuleMixedWithSpecificRule() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "ALL", null), new RuleCommand("DEPT_CODE", "=", "APAC")), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("ALL"); } @Test void rejectsCustomPredicateRule() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand("DEPT_CODE", "CUSTOM_PREDICATE", "1=1")), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("허용되지 않은"); } @Test void allowsDefaultColumnsForContextRules() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "MY_DEPT", null), new RuleCommand(null, "SELF", null)), List.of() ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleType) .containsExactly("MY_DEPT", "SELF"); assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleColumn) .containsExactly(null, null); } @Test void acceptsDeptAndEmpNoRulesWithDefaultColumnsAndValues() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "DEPT", "HR"), new RuleCommand(null, "EMP_NO", "E2001")), List.of() ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleType) .containsExactly("DEPT", "EMP_NO"); assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleValue) .containsExactly("HR", "E2001"); } @Test void rejectsDeptRuleWithoutValue() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "DEPT", "")), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("값이 필요"); } @Test void acceptsMultipleTagRulesAsOrCandidates() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of( new RuleCommand(null, "TAG", "spring_boot"), new RuleCommand(null, "TAG", "oracle_vpd") ), List.of() ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleType) .containsExactly("TAG", "TAG"); assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleColumn) .containsExactly(null, null); assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleValue) .containsExactly("SPRING_BOOT", "ORACLE_VPD"); } @Test void acceptsStakeholderContextRulesWithAnExplicitObjectColumn() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of( new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", "설계사"), new RuleCommand("OWNER_EMP_NO", "STAKEHOLDER_CHANNEL", "지점장") ), List.of() ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedRules) .extracting(PermissionRule::ruleType) .containsExactly("STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL"); } @Test void rejectsStakeholderContextRuleWithoutAnObjectColumn() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "STAKEHOLDER_SELF", null)), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("컬럼이 필요"); } @Test void rejectsStakeholderContextRuleWithoutExpectedRole() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand("DEPT_CODE", "STAKEHOLDER_SELF", null)), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("값이 필요"); } @Test void acceptsMultipleVisibleColumns() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand("DEPT_CODE", "=", "HR")), List.of("DEPT_CODE", "OWNER_EMP_NO") ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedVisibleColumns).containsExactly("DEPT_CODE", "OWNER_EMP_NO"); } @Test void rejectsTagRuleWithoutValue() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "TAG", "")), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("값이 필요"); } @Test void rejectsUnsafeTagValue() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "ALLOW", List.of(new RuleCommand(null, "TAG", "SPRING' OR '1'='1")), List.of() ); assertThatThrownBy(() -> permissionService.savePermissionSet(command)) .isInstanceOf(AppException.class) .hasMessageContaining("영문 대문자"); } @Test void acceptsDenyPermissionEffect() { var command = new PermissionSetCommand( 10L, 1L, "SELECT", "DENY", List.of(new RuleCommand("DEPT_CODE", "=", "HR")), List.of() ); permissionService.savePermissionSet(command); FakePermissionMapper mapper = (FakePermissionMapper) permissionMapper; assertThat(mapper.insertedEffect).isEqualTo("DENY"); } @Test void disablesProtectedObjectWhenLastPermissionIsDeleted() { var mapper = new FakePermissionMapper(); boolean[] disabled = {false}; AuditService auditService = new AuditService(new NoopAuditMapper()); ProtectedObjectService objectService = new ProtectedObjectService(null, auditService) { @Override public void disableObject(long objectId) { disabled[0] = objectId == 1L; } }; PermissionService service = new PermissionService(mapper, objectService, auditService); service.deletePermission(1000L); assertThat(disabled[0]).isTrue(); } @Test void rejectsPermissionDeleteWithoutImpactConfirmation() { assertThatThrownBy(() -> permissionService.deletePermission(1000L, false)) .isInstanceOf(AppException.class) .hasMessageContaining("영향 확인"); } @Test void blocksRoleDeleteWhenDependenciesRemain() { var mapper = new FakePermissionMapper(); mapper.userRoleCount = 1; PermissionService service = new PermissionService(mapper, protectedObjectService, new AuditService(new NoopAuditMapper())); assertThatThrownBy(() -> service.deleteRole(10L, true)) .isInstanceOf(AppException.class) .hasMessageContaining("먼저 해제"); } private static class NoopAuditMapper implements AuditMapper { @Override public void insert(AuditEvent event) { } } private static class FakePermissionMapper implements PermissionMapper { private final List insertedRules = new ArrayList<>(); private final List insertedVisibleColumns = new ArrayList<>(); @Override public List findRoles() { return List.of(new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL")); } @Override public AppRole findRole(long roleId) { return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null, "INTERNAL") : null; } @Override public long nextRoleId() { return 40L; } @Override public void insertRole(long roleId, String roleName, String description, String maxSensitivityLevel) { } @Override public int updateRoleMaxSensitivity(long roleId, String maxSensitivityLevel) { return 1; } @Override public int deleteRole(long roleId) { return 1; } private int userRoleCount; private int groupRoleCount; private int permissionCountByRole; @Override public int countUserRolesByRoleId(long roleId) { return userRoleCount; } @Override public int countGroupRolesByRoleId(long roleId) { return groupRoleCount; } @Override public int countPermissionsByRoleId(long roleId) { return permissionCountByRole; } @Override public List findPermissionViews() { return List.of(); } @Override public PermissionSet findPermissionSet(long roleId, long objectId) { return null; } @Override public Long findPermissionId(long roleId, long objectId) { return null; } @Override public Long findObjectIdByPermissionId(long permissionId) { return permissionId == 1000L ? 1L : null; } @Override public int countPermissionsByObjectId(long objectId) { return 0; } private String insertedEffect; @Override public void insertPermission(long permissionId, long roleId, long objectId, String action, String permissionEffect) { insertedEffect = permissionEffect; } @Override public void updatePermissionAction(long permissionId, String action) { } @Override public void updatePermissionEffect(long permissionId, String permissionEffect) { insertedEffect = permissionEffect; } @Override public void deleteRules(long permissionId) { } @Override public void insertRule(PermissionRule rule) { insertedRules.add(rule); } @Override public void deleteVisibleColumns(long permissionId) { } @Override public void insertVisibleColumn(long permissionId, String columnName) { insertedVisibleColumns.add(columnName); } @Override public int deletePermission(long permissionId) { return 1; } @Override public long nextPermissionId() { return 1000L; } @Override public long nextRuleId() { return 10000L; } } }