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()); } }