test: ga-common 서비스 + ga-batch MappingEngine 단위 테스트 37건 추가

- MenuTreeBuilderTest (6): 트리 빌드, sortOrder 정렬, 고아 노드 처리
- PermissionCheckerTest (4): userId null 단락, mapper 위임
- CommonCodeServiceTest (14): 그룹/상세 CRUD, 시스템 코드 잠금, 중복 검출
- MappingEngineTest (13): RECRUIT/MAINTAIN 라우팅, TRIM/UPPER/LOWER/DATE/
  DIVIDE/MULTIPLY/CODE_MAP 변환, 9가지 에러 분기

전체 단위 테스트 76건 (1+2단계 합산).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-10 20:53:30 +09:00
parent 8a0483cccc
commit 936d376794
4 changed files with 761 additions and 0 deletions
@@ -0,0 +1,209 @@
package com.ga.common.code;
import com.ga.common.exception.BizException;
import com.ga.common.exception.ErrorCode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* CommonCodeService 단위 테스트.
*
* 주의: @Cacheable / @CacheEvict 는 Spring 프록시 단계에서 동작하므로
* 단위 테스트(직접 인스턴스 호출)에서는 캐시가 끼지 않는다 — 비즈니스 로직만 검증.
*/
@ExtendWith(MockitoExtension.class)
class CommonCodeServiceTest {
@Mock CommonCodeMapper mapper;
@InjectMocks CommonCodeService service;
private CommonCodeVO code(String groupCode, String code, String name) {
CommonCodeVO c = new CommonCodeVO();
c.setGroupCode(groupCode);
c.setCode(code);
c.setCodeName(name);
return c;
}
private CommonCodeGroupVO group(String groupCode, String isSystem) {
CommonCodeGroupVO g = new CommonCodeGroupVO();
g.setGroupCode(groupCode);
g.setIsSystem(isSystem);
return g;
}
/** 필요한 테스트에서만 호출 (strict stubbing) */
private void stubGenderGroup() {
when(mapper.selectActiveCodes("GENDER")).thenReturn(List.of(
code("GENDER", "M", "남자"),
code("GENDER", "F", "여자")
));
}
@Test
@DisplayName("getCodeName: 매칭 성공")
void getCodeName_hit() {
stubGenderGroup();
assertThat(service.getCodeName("GENDER", "M")).isEqualTo("남자");
assertThat(service.getCodeName("GENDER", "F")).isEqualTo("여자");
}
@Test
@DisplayName("getCodeName: 미존재 코드 → null")
void getCodeName_miss() {
stubGenderGroup();
assertThat(service.getCodeName("GENDER", "X")).isNull();
}
@Test
@DisplayName("getCodeName: code null → null (DB 조회 안함)")
void getCodeName_nullCode() {
assertThat(service.getCodeName("GENDER", null)).isNull();
verify(mapper, never()).selectActiveCodes(any());
}
@Test
@DisplayName("validateCode: 미존재 시 INVALID_PARAMETER 예외")
void validateCode_invalid() {
stubGenderGroup();
assertThatThrownBy(() -> service.validateCode("GENDER", "X"))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.INVALID_PARAMETER);
}
@Test
@DisplayName("validateCode: 존재하면 예외 없음")
void validateCode_ok() {
stubGenderGroup();
service.validateCode("GENDER", "M"); // no throw
}
@Test
@DisplayName("getGroup: 미존재 시 NOT_FOUND")
void getGroup_notFound() {
when(mapper.selectGroup("MISSING")).thenReturn(null);
assertThatThrownBy(() -> service.getGroup("MISSING"))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.NOT_FOUND);
}
@Test
@DisplayName("createGroup: 중복 그룹 코드 → DUPLICATE_DATA")
void createGroup_duplicate() {
when(mapper.selectGroup("DUP")).thenReturn(group("DUP", "N"));
CommonCodeGroupVO vo = group("DUP", "N");
assertThatThrownBy(() -> service.createGroup(vo))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.DUPLICATE_DATA);
verify(mapper, never()).insertGroup(any());
}
@Test
@DisplayName("createGroup: 신규 → insertGroup 호출")
void createGroup_ok() {
when(mapper.selectGroup("NEW")).thenReturn(null);
CommonCodeGroupVO vo = group("NEW", "N");
service.createGroup(vo);
verify(mapper).insertGroup(vo);
}
@Test
@DisplayName("updateGroup: 시스템 그룹은 SYSTEM_CODE_LOCKED")
void updateGroup_systemLocked() {
when(mapper.selectGroup("SYS")).thenReturn(group("SYS", "Y"));
CommonCodeGroupVO vo = group("SYS", "Y");
assertThatThrownBy(() -> service.updateGroup(vo))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
verify(mapper, never()).updateGroup(any());
}
@Test
@DisplayName("deleteGroup: 일반 그룹은 삭제 가능")
void deleteGroup_ok() {
when(mapper.selectGroup("USER_GRP")).thenReturn(group("USER_GRP", "N"));
service.deleteGroup("USER_GRP");
verify(mapper).deleteGroup("USER_GRP");
}
@Test
@DisplayName("deleteGroup: 시스템 그룹은 SYSTEM_CODE_LOCKED")
void deleteGroup_systemLocked() {
when(mapper.selectGroup("SYS")).thenReturn(group("SYS", "Y"));
assertThatThrownBy(() -> service.deleteGroup("SYS"))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
verify(mapper, never()).deleteGroup(any());
}
@Test
@DisplayName("createCode: 동일 (group, code) 중복 → DUPLICATE_DATA")
void createCode_duplicate() {
when(mapper.existsByGroupAndCode("GENDER", "M")).thenReturn(1);
assertThatThrownBy(() -> service.createCode(code("GENDER", "M", "남자")))
.isInstanceOf(BizException.class);
verify(mapper, never()).insertCode(any());
}
@Test
@DisplayName("updateCode: 시스템 그룹의 code/groupCode 는 강제로 원본 유지")
void updateCode_systemForcesIdentity() {
CommonCodeVO old = code("GENDER", "M", "남자");
old.setCodeId(1L);
when(mapper.selectCode(1L)).thenReturn(old);
when(mapper.selectGroup("GENDER")).thenReturn(group("GENDER", "Y")); // 시스템 그룹
// 사용자가 임의로 code/groupCode 바꾸려 시도
CommonCodeVO update = code("OTHER", "X", "변경시도");
update.setCodeId(1L);
service.updateCode(update);
// 시스템 그룹이므로 code/groupCode 가 원본으로 강제 복원됨
assertThat(update.getCode()).isEqualTo("M");
assertThat(update.getGroupCode()).isEqualTo("GENDER");
verify(mapper).updateCode(update);
}
@Test
@DisplayName("deleteCode: 시스템 그룹은 SYSTEM_CODE_LOCKED")
void deleteCode_systemLocked() {
CommonCodeVO c = code("GENDER", "M", "남자");
c.setCodeId(1L);
when(mapper.selectCode(1L)).thenReturn(c);
when(mapper.selectGroup("GENDER")).thenReturn(group("GENDER", "Y"));
assertThatThrownBy(() -> service.deleteCode(1L))
.isInstanceOf(BizException.class)
.extracting(e -> ((BizException) e).getErrorCode())
.isEqualTo(ErrorCode.SYSTEM_CODE_LOCKED);
verify(mapper, never()).deleteCode(any());
}
}
@@ -0,0 +1,113 @@
package com.ga.common.menu;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class MenuTreeBuilderTest {
private MenuVO menu(Long id, Long parentId, String code, int sort) {
MenuVO m = new MenuVO();
m.setMenuId(id);
m.setParentMenuId(parentId);
m.setMenuCode(code);
m.setMenuName(code + "-name");
m.setMenuType("PAGE");
m.setSortOrder(sort);
m.setIsActive("Y");
return m;
}
@Test
@DisplayName("부모-자식 연결: parent_menu_id 가 null 이면 root")
void buildTree_basic() {
List<MenuVO> flat = List.of(
menu(1L, null, "ROOT_A", 1),
menu(2L, 1L, "A_CHILD_1", 2),
menu(3L, 1L, "A_CHILD_2", 1),
menu(4L, null, "ROOT_B", 2)
);
List<MenuResp> roots = MenuTreeBuilder.build(flat, Map.of());
assertThat(roots).hasSize(2);
// root 도 sortOrder 로 정렬됨
assertThat(roots.get(0).getMenuCode()).isEqualTo("ROOT_A");
assertThat(roots.get(1).getMenuCode()).isEqualTo("ROOT_B");
// ROOT_A 의 자식이 sortOrder 순으로 정렬되었는지
List<MenuResp> aChildren = roots.get(0).getChildren();
assertThat(aChildren).hasSize(2);
assertThat(aChildren.get(0).getMenuCode()).isEqualTo("A_CHILD_2"); // sort 1
assertThat(aChildren.get(1).getMenuCode()).isEqualTo("A_CHILD_1"); // sort 2
assertThat(roots.get(1).getChildren()).isEmpty();
}
@Test
@DisplayName("permissionsByMenuId: menuId 별 권한 코드 주입")
void permissions_injected() {
List<MenuVO> flat = List.of(menu(10L, null, "M", 1));
Map<Long, List<String>> perms = Map.of(10L, List.of("READ", "CREATE", "UPDATE"));
List<MenuResp> roots = MenuTreeBuilder.build(flat, perms);
assertThat(roots.get(0).getPermissions()).containsExactly("READ", "CREATE", "UPDATE");
}
@Test
@DisplayName("permissionsByMenuId 없는 메뉴는 빈 리스트")
void permissions_emptyDefault() {
List<MenuVO> flat = List.of(menu(10L, null, "M", 1));
List<MenuResp> roots = MenuTreeBuilder.build(flat, Map.of());
assertThat(roots.get(0).getPermissions()).isEmpty();
}
@Test
@DisplayName("부모가 목록에 없는 고아 메뉴는 root 로 끌어올림")
void orphan_promotedToRoot() {
List<MenuVO> flat = List.of(
menu(1L, null, "ROOT", 1),
menu(99L, 999L, "ORPHAN", 1) // 부모 999가 목록에 없음
);
List<MenuResp> roots = MenuTreeBuilder.build(flat, Map.of());
// 둘 다 root 로 처리되어야 함
assertThat(roots).hasSize(2);
assertThat(roots).extracting(MenuResp::getMenuCode).containsExactlyInAnyOrder("ROOT", "ORPHAN");
}
@Test
@DisplayName("3단계 계층: 손자까지 연결되고 각 레벨 sortOrder 정렬")
void threeLevels() {
List<MenuVO> flat = List.of(
menu(1L, null, "L1", 1),
menu(2L, 1L, "L2", 1),
menu(3L, 2L, "L3_B", 2),
menu(4L, 2L, "L3_A", 1)
);
List<MenuResp> roots = MenuTreeBuilder.build(flat, Map.of());
MenuResp l2 = roots.get(0).getChildren().get(0);
assertThat(l2.getMenuCode()).isEqualTo("L2");
assertThat(l2.getChildren()).extracting(MenuResp::getMenuCode)
.containsExactly("L3_A", "L3_B"); // sortOrder 정렬
}
@Test
@DisplayName("permissionsByMenuId 가 null 이면 권한 주입 스킵")
void permissionsByMenuId_null_skipsInjection() {
List<MenuVO> flat = List.of(menu(10L, null, "M", 1));
List<MenuResp> roots = MenuTreeBuilder.build(flat, null);
// 기본값(빈 리스트) 그대로 유지
assertThat(roots.get(0).getPermissions()).isEmpty();
}
}
@@ -0,0 +1,57 @@
package com.ga.common.menu;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class PermissionCheckerTest {
@Mock MenuMapper menuMapper;
@InjectMocks PermissionChecker checker;
@Test
@DisplayName("userId null 이면 즉시 false 반환 (DB 조회 안함)")
void nullUser_returnsFalse() {
boolean ok = checker.hasPermission(null, "MENU", "READ");
assertThat(ok).isFalse();
verify(menuMapper, never()).hasPermission(any(), any(), any());
}
@Test
@DisplayName("Mapper 가 1 이상 반환 → true")
void hasPermission_grant() {
when(menuMapper.hasPermission(1L, "AGENT", "READ")).thenReturn(1);
assertThat(checker.hasPermission(1L, "AGENT", "READ")).isTrue();
}
@Test
@DisplayName("Mapper 가 0 반환 → false")
void hasPermission_deny() {
when(menuMapper.hasPermission(1L, "AGENT", "DELETE")).thenReturn(0);
assertThat(checker.hasPermission(1L, "AGENT", "DELETE")).isFalse();
}
@Test
@DisplayName("Mapper 호출 인자가 그대로 전달됨")
void argumentsForwarded() {
when(menuMapper.hasPermission(anyLong(), any(), any())).thenReturn(1);
checker.hasPermission(42L, "SETTLE", "APPROVE");
verify(menuMapper).hasPermission(42L, "SETTLE", "APPROVE");
}
}