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:
@@ -0,0 +1,382 @@
|
||||
package com.ga.batch.settlement.transform;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.product.ContractMapper;
|
||||
import com.ga.core.mapper.receive.ReceiveMapper;
|
||||
import com.ga.core.vo.ledger.LedgerVO;
|
||||
import com.ga.core.vo.product.ContractResp;
|
||||
import com.ga.core.vo.product.ContractSearchParam;
|
||||
import com.ga.core.vo.product.ContractVO;
|
||||
import com.ga.core.vo.receive.CompanyFieldMappingVO;
|
||||
import com.ga.core.vo.receive.ParseErrorLogVO;
|
||||
import com.ga.core.vo.receive.RawCommissionDataVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MappingEngineTest {
|
||||
|
||||
private ReceiveMapper receiveMapper;
|
||||
private ContractMapper contractMapper;
|
||||
private RecruitLedgerMapper recruitMapper;
|
||||
private MaintainLedgerMapper maintainMapper;
|
||||
private MappingEngine engine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
receiveMapper = mock(ReceiveMapper.class);
|
||||
contractMapper = mock(ContractMapper.class);
|
||||
recruitMapper = mock(RecruitLedgerMapper.class);
|
||||
maintainMapper = mock(MaintainLedgerMapper.class);
|
||||
engine = new MappingEngine(receiveMapper, contractMapper, recruitMapper, maintainMapper, new ObjectMapper());
|
||||
}
|
||||
|
||||
/** 매핑 정의 1건 생성 헬퍼 */
|
||||
private CompanyFieldMappingVO mapping(String src, String tgt, String dataType, String rule, String defVal, String required) {
|
||||
CompanyFieldMappingVO m = new CompanyFieldMappingVO();
|
||||
m.setSourceField(src);
|
||||
m.setTargetField(tgt);
|
||||
m.setDataType(dataType);
|
||||
m.setTransformRule(rule);
|
||||
m.setDefaultValue(defVal);
|
||||
m.setIsRequired(required);
|
||||
return m;
|
||||
}
|
||||
|
||||
/** raw 1건 + 정상 mapping 정의 + 계약 매칭까지 갖춘 행복 경로 셋업 */
|
||||
private RawCommissionDataVO basicRecruitRaw(String json) {
|
||||
RawCommissionDataVO raw = new RawCommissionDataVO();
|
||||
raw.setRawId(1000L);
|
||||
raw.setCompanyCode("SAMSUNG");
|
||||
raw.setSettleMonth("202405");
|
||||
raw.setDataType("RECRUIT");
|
||||
raw.setRawContent(json);
|
||||
return raw;
|
||||
}
|
||||
|
||||
private void setupContractMatch(String policyNo, Long contractId, Long agentId) {
|
||||
ContractResp resp = new ContractResp();
|
||||
resp.setContractId(contractId);
|
||||
when(contractMapper.selectList(any(ContractSearchParam.class)))
|
||||
.thenReturn(List.of(resp));
|
||||
|
||||
ContractVO vo = new ContractVO();
|
||||
vo.setContractId(contractId);
|
||||
vo.setAgentId(agentId);
|
||||
vo.setPolicyNo(policyNo);
|
||||
vo.setPremium(new BigDecimal("500000"));
|
||||
vo.setPayCycle("MONTHLY");
|
||||
when(contractMapper.selectById(contractId)).thenReturn(vo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("RECRUIT 행복경로: TRIM/DATE/CODE_MAP 모두 적용 + recruitMapper.insert 호출")
|
||||
void recruit_happyPath() {
|
||||
String json = """
|
||||
{"policyNo":" POL-001 ", "premium":"1000000", "amt":"75000",
|
||||
"agentAmt":"45000", "compRate":"7.5", "payRate":"60",
|
||||
"type":"WL_RAW"}
|
||||
""";
|
||||
RawCommissionDataVO raw = basicRecruitRaw(json);
|
||||
|
||||
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
|
||||
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y"),
|
||||
mapping("premium", "premium", "DECIMAL", null, null, "N"),
|
||||
mapping("amt", "company_amount", "DECIMAL", null, null, "N"),
|
||||
mapping("agentAmt", "agent_amount", "DECIMAL", null, null, "N"),
|
||||
mapping("compRate", "company_rate", "DECIMAL", null, null, "N"),
|
||||
mapping("payRate", "payout_rate", "DECIMAL", null, null, "N"),
|
||||
mapping("type", "insurance_type", "STRING", "CODE_MAP", "INSURANCE_TYPE", "N")
|
||||
));
|
||||
when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "WL_RAW")).thenReturn("WHOLE_LIFE");
|
||||
setupContractMatch("POL-001", 100L, 10L);
|
||||
|
||||
Long ledgerId = engine.transform(raw);
|
||||
|
||||
// recruit insert 호출 / maintain 미호출
|
||||
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
|
||||
verify(recruitMapper).insert(cap.capture());
|
||||
verify(maintainMapper, never()).insert(any());
|
||||
|
||||
LedgerVO led = cap.getValue();
|
||||
assertThat(led.getContractId()).isEqualTo(100L);
|
||||
assertThat(led.getAgentId()).isEqualTo(10L);
|
||||
assertThat(led.getPolicyNo()).isEqualTo("POL-001"); // TRIM
|
||||
assertThat(led.getCompanyCode()).isEqualTo("SAMSUNG");
|
||||
assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE"); // CODE_MAP 변환
|
||||
assertThat(led.getPremium()).isEqualByComparingTo("1000000");
|
||||
assertThat(led.getCompanyAmount()).isEqualByComparingTo("75000");
|
||||
assertThat(led.getAgentAmount()).isEqualByComparingTo("45000");
|
||||
assertThat(led.getCompanyRate()).isEqualByComparingTo("7.5");
|
||||
assertThat(led.getPayoutRate()).isEqualByComparingTo("60");
|
||||
// 세금은 agent_amount × 3.3% 자동 계산: 45000 × 3.3% = 1485
|
||||
assertThat(led.getTaxAmount()).isEqualByComparingTo("1485.00");
|
||||
assertThat(led.getSettleMonth()).isEqualTo("202405");
|
||||
assertThat(led.getStatus()).isEqualTo("CALCULATED");
|
||||
assertThat(led.getReconcileStatus()).isEqualTo("PENDING");
|
||||
// ledger 의 ID 는 insert 후 채워지지만, 반환값은 mapped LedgerId 그대로
|
||||
assertThat(ledgerId).isEqualTo(led.getLedgerId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MAINTAIN 데이터타입은 maintainMapper.insert 로 라우팅")
|
||||
void maintain_routedToMaintainMapper() {
|
||||
String json = """
|
||||
{"policyNo":"POL-002", "premium":"500000"}
|
||||
""";
|
||||
RawCommissionDataVO raw = new RawCommissionDataVO();
|
||||
raw.setRawId(2000L);
|
||||
raw.setCompanyCode("HANWHA");
|
||||
raw.setSettleMonth("202405");
|
||||
raw.setDataType("MAINTAIN");
|
||||
raw.setRawContent(json);
|
||||
|
||||
when(receiveMapper.selectFieldMappings("HANWHA", "maintain_ledger")).thenReturn(List.of(
|
||||
mapping("policyNo", "policy_no", "STRING", null, null, "Y"),
|
||||
mapping("premium", "premium", "DECIMAL", null, null, "N")
|
||||
));
|
||||
setupContractMatch("POL-002", 200L, 20L);
|
||||
|
||||
engine.transform(raw);
|
||||
|
||||
verify(maintainMapper).insert(any(LedgerVO.class));
|
||||
verify(recruitMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("UPPER/LOWER/DIVIDE/MULTIPLY 변환 규칙 검증")
|
||||
void multipleTransformRules() {
|
||||
String json = """
|
||||
{"upper":"hello", "lower":"WORLD",
|
||||
"div":"100000", "mul":"5"}
|
||||
""";
|
||||
RawCommissionDataVO raw = basicRecruitRaw(json);
|
||||
// policy_no 도 필수이므로 추가
|
||||
String j2 = json.replace("{", "{\"pol\":\"POL-X\",");
|
||||
raw.setRawContent(j2);
|
||||
|
||||
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
|
||||
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
|
||||
// attr1 등 표준 외 필드는 mapped 에 들어가지만 LedgerVO 에 매핑되진 않음 — 변환만 검증
|
||||
mapping("upper", "product_code", "STRING", "UPPER", null, "N"),
|
||||
mapping("lower", "pay_method", "STRING", "LOWER", null, "N"),
|
||||
mapping("div", "agent_amount", "DECIMAL", "DIVIDE", "100", "N"),
|
||||
mapping("mul", "company_amount", "DECIMAL", "MULTIPLY", "1000","N")
|
||||
));
|
||||
setupContractMatch("POL-X", 300L, 30L);
|
||||
|
||||
engine.transform(raw);
|
||||
|
||||
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
|
||||
verify(recruitMapper).insert(cap.capture());
|
||||
LedgerVO led = cap.getValue();
|
||||
|
||||
assertThat(led.getProductCode()).isEqualTo("HELLO"); // UPPER
|
||||
assertThat(led.getPayMethod()).isEqualTo("world"); // LOWER
|
||||
// DIVIDE: 100000 / 100 = 1000.00
|
||||
assertThat(led.getAgentAmount()).isEqualByComparingTo("1000.00");
|
||||
// MULTIPLY: 5 × 1000 = 5000
|
||||
assertThat(led.getCompanyAmount()).isEqualByComparingTo("5000");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("DATE 변환: defaultValue 의 패턴으로 파싱")
|
||||
void dateTransform() {
|
||||
// mapped 의 "contract_date" 등은 LedgerVO 에 직접 안 들어가므로,
|
||||
// 변환만 일어나고 buildLedger 에서 기본값을 쓰는지 확인하기 어려움.
|
||||
// 대신 변환 자체가 실패해서 에러가 안 나는지로 확인.
|
||||
String json = """
|
||||
{"pol":"POL-D", "dateField":"20240515"}
|
||||
""";
|
||||
RawCommissionDataVO raw = basicRecruitRaw(json);
|
||||
raw.setRawContent(json);
|
||||
|
||||
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger")).thenReturn(List.of(
|
||||
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
|
||||
mapping("dateField", "contract_date", "DATE", "DATE", "yyyyMMdd", "N")
|
||||
));
|
||||
setupContractMatch("POL-D", 400L, 40L);
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
// 정상 처리 (예외 없음)
|
||||
verify(recruitMapper).insert(any(LedgerVO.class));
|
||||
verify(receiveMapper, never()).insertError(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("INVALID_INPUT: company_code 가 null 이면 에러 로그 + null 반환")
|
||||
void invalidInput_nullCompanyCode() {
|
||||
RawCommissionDataVO raw = new RawCommissionDataVO();
|
||||
raw.setRawId(1L);
|
||||
raw.setRawContent("{}");
|
||||
raw.setDataType("RECRUIT");
|
||||
// companyCode 누락
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("INVALID_INPUT");
|
||||
verify(recruitMapper, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("UNKNOWN_TYPE: 지원하지 않는 data_type")
|
||||
void unknownDataType() {
|
||||
RawCommissionDataVO raw = new RawCommissionDataVO();
|
||||
raw.setRawId(2L);
|
||||
raw.setCompanyCode("SAMSUNG");
|
||||
raw.setRawContent("{}");
|
||||
raw.setDataType("STRANGE");
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("UNKNOWN_TYPE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("NO_MAPPING: 매핑 정의가 비어있음")
|
||||
void noMapping() {
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{}");
|
||||
when(receiveMapper.selectFieldMappings("SAMSUNG", "recruit_ledger"))
|
||||
.thenReturn(new ArrayList<>());
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("NO_MAPPING");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JSON_PARSE: 잘못된 raw_content")
|
||||
void jsonParseError() {
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{not-json");
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("a", "policy_no", "STRING", null, null, "Y")
|
||||
));
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("JSON_PARSE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("REQUIRED: 필수 필드가 raw 에 없음")
|
||||
void requiredMissing() {
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{\"other\":\"x\"}");
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y") // 필수
|
||||
));
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("REQUIRED");
|
||||
assertThat(cap.getValue().getErrorField()).isEqualTo("policy_no");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("NO_POLICY: policy_no 매핑이 아예 정의되지 않음")
|
||||
void noPolicyMapping() {
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{\"premium\":\"100\"}");
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("premium", "premium", "DECIMAL", null, null, "N")
|
||||
));
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("NO_POLICY");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("CONTRACT_NOT_FOUND: policy_no 로 계약 매칭 실패")
|
||||
void contractNotFound() {
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{\"policyNo\":\"POL-NONE\"}");
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("policyNo", "policy_no", "STRING", "TRIM", null, "Y")
|
||||
));
|
||||
when(contractMapper.selectList(any(ContractSearchParam.class))).thenReturn(List.of());
|
||||
|
||||
Long id = engine.transform(raw);
|
||||
|
||||
assertThat(id).isNull();
|
||||
ArgumentCaptor<ParseErrorLogVO> cap = ArgumentCaptor.forClass(ParseErrorLogVO.class);
|
||||
verify(receiveMapper).insertError(cap.capture());
|
||||
assertThat(cap.getValue().getErrorType()).isEqualTo("CONTRACT_NOT_FOUND");
|
||||
assertThat(cap.getValue().getErrorValue()).isEqualTo("POL-NONE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("CODE_MAP: 매칭 실패 시 원본 값 그대로 사용")
|
||||
void codeMap_fallback() {
|
||||
String json = """
|
||||
{"pol":"POL-CM", "type":"UNKNOWN_TYPE"}
|
||||
""";
|
||||
RawCommissionDataVO raw = basicRecruitRaw(json);
|
||||
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y"),
|
||||
mapping("type", "insurance_type", "STRING", "CODE_MAP", "INSURANCE_TYPE", "N")
|
||||
));
|
||||
when(receiveMapper.findTargetCode("SAMSUNG", "INSURANCE_TYPE", "UNKNOWN_TYPE")).thenReturn(null);
|
||||
setupContractMatch("POL-CM", 500L, 50L);
|
||||
|
||||
engine.transform(raw);
|
||||
|
||||
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
|
||||
verify(recruitMapper).insert(cap.capture());
|
||||
// 매핑 미존재 → 원본 그대로
|
||||
assertThat(cap.getValue().getInsuranceType()).isEqualTo("UNKNOWN_TYPE");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("계약값 폴백: premium/pay_method 가 mapped 에 없으면 contract 값 사용")
|
||||
void buildLedger_fallbackToContract() {
|
||||
// premium, pay_method 매핑 없음 → contract 의 값(500000, MONTHLY) 사용
|
||||
RawCommissionDataVO raw = basicRecruitRaw("{\"pol\":\"POL-FB\"}");
|
||||
when(receiveMapper.selectFieldMappings(any(), any())).thenReturn(List.of(
|
||||
mapping("pol", "policy_no", "STRING", "TRIM", null, "Y")
|
||||
));
|
||||
setupContractMatch("POL-FB", 600L, 60L);
|
||||
|
||||
engine.transform(raw);
|
||||
|
||||
ArgumentCaptor<LedgerVO> cap = ArgumentCaptor.forClass(LedgerVO.class);
|
||||
verify(recruitMapper).insert(cap.capture());
|
||||
LedgerVO led = cap.getValue();
|
||||
assertThat(led.getPremium()).isEqualByComparingTo("500000"); // contract.premium
|
||||
assertThat(led.getPayMethod()).isEqualTo("MONTHLY"); // contract.payCycle
|
||||
assertThat(led.getInsuranceType()).isEqualTo("WHOLE_LIFE"); // 기본값
|
||||
assertThat(led.getCommissionYear()).isEqualTo(1); // 기본값
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user