From bf03a97fa357ea399d62bad9d461e46a459f4cf0 Mon Sep 17 00:00:00 2001 From: curry772 Date: Mon, 15 Jun 2026 14:45:18 +0900 Subject: [PATCH] =?UTF-8?q?=EC=98=A4=EB=A5=98=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A5=BC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=8D=BC=ED=8B=B0=20=EA=B7=B8=EB=A3=B9=EC=97=90=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=ED=95=98=EC=97=AC=20=EB=A7=A4=ED=95=91=20=EC=B9=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8A=94=20AdapterErrorMsgHandler=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AdapterErrorMessageHandler{CODE_CONVERT} 에 등록 - TemplateCodeConvertAdapterErrorMsgHandler json만 지원하는 handler 개발 --- .../TemplateAdapterErrorMsgHandler.java | 2 +- ...lateCodeConvertAdapterErrorMsgHandler.java | 126 ++++ ...CodeConvertAdapterErrorMsgHandlerTest.java | 549 ++++++++++++++++++ 3 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java create mode 100644 src/test/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandlerTest.java diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java index 4211761..929bc5b 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -89,7 +89,7 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); - /** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */ + /** PropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */ static final String PROP_GROUP = "AdapterErrorMessageHandler"; /** callProp 참조 변수 접두어: ${callprop.키} */ diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java new file mode 100644 index 0000000..fb33f04 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java @@ -0,0 +1,126 @@ +package com.eactive.eai.adapter.handler; + +import java.util.Properties; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.common.message.EAIMessage; +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.Logger; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + + +public class TemplateCodeConvertAdapterErrorMsgHandler extends TemplateAdapterErrorMsgHandler { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + /** PropManager 에서 코드 변환 설정을 조회할 프로퍼티 그룹 이름 */ + static final String PROP_GROUP = "AdapterErrorMessageHandler{CODE_CONVERT}"; + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Override + public Object generateNonStandardErrorResponseMessage( + String inboundAdapterGroupName, + String inboundAdapterName, + Properties callProp, + Object outboundRequestData, + EAIMessage resEaiMsg) throws Exception { + + Object responseMsessage = super.generateNonStandardErrorResponseMessage( + inboundAdapterGroupName, + inboundAdapterName, + callProp, + outboundRequestData, + resEaiMsg); + + logger.debug("generateNonStandardErrorResponseMessage - {}", responseMsessage); + + if (!(responseMsessage instanceof String)) { + return responseMsessage; + } + + String jsonStr = (String) responseMsessage; + if (StringUtils.isBlank(jsonStr)) { + return responseMsessage; + } + + Properties props = PropManager.getInstance().getProperties(PROP_GROUP); + if (props == null) { + return responseMsessage; + } + + String fieldsKey = inboundAdapterGroupName + ".convert.fields"; + String fieldsValue = props.getProperty(fieldsKey); + if (StringUtils.isBlank(fieldsValue)) { + return responseMsessage; + } + + JsonNode rootNode = OBJECT_MAPPER.readTree(jsonStr); + boolean modified = false; + + for (String rawField : fieldsValue.split(",")) { + String fieldPath = rawField.trim(); + if (StringUtils.isBlank(fieldPath)) continue; + + String currentValue = getJsonValue(rootNode, fieldPath); + if (currentValue == null) continue; + + // key: inboundAdapterGroupName.{fieldPath}.{현재값} → 폴백: ...{fieldPath}.default + String mappingKey = inboundAdapterGroupName + "." + fieldPath + "." + currentValue; + String newValue = props.getProperty(mappingKey); + if (newValue == null) { + String defaultKey = inboundAdapterGroupName + "." + fieldPath + ".default"; + newValue = props.getProperty(defaultKey); + } + if (newValue == null) continue; + + setJsonValue(rootNode, fieldPath, newValue); + modified = true; + + if (logger.isDebug()) { + logger.debug("code convert: field={}, {} -> {}", fieldPath, currentValue, newValue); + } + } + + if (modified) { + responseMsessage = OBJECT_MAPPER.writeValueAsString(rootNode); + } + + return responseMsessage; + } + + /** + * dot 구분 경로로 JsonNode 에서 값을 꺼낸다. + * 최종 노드가 값 노드(primitive)가 아니면 null 반환. + */ + private String getJsonValue(JsonNode node, String fieldPath) { + JsonNode current = node; + for (String part : fieldPath.split("\\.", -1)) { + if (current == null || !current.isObject()) return null; + current = current.get(part); + } + if (current == null || current.isNull() || !current.isValueNode()) return null; + return current.asText(); + } + + /** + * dot 구분 경로로 ObjectNode 의 최종 필드 값을 newValue 로 교체한다. + * 중간 경로가 ObjectNode 가 아니면 무시한다. + */ + private void setJsonValue(JsonNode rootNode, String fieldPath, String newValue) { + String[] parts = fieldPath.split("\\.", -1); + JsonNode current = rootNode; + for (int i = 0; i < parts.length - 1; i++) { + if (current == null || !current.isObject()) return; + current = current.get(parts[i]); + } + if (current instanceof ObjectNode) { + ((ObjectNode) current).put(parts[parts.length - 1], newValue); + } + } + + +} diff --git a/src/test/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandlerTest.java b/src/test/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandlerTest.java new file mode 100644 index 0000000..12878f0 --- /dev/null +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandlerTest.java @@ -0,0 +1,549 @@ +package com.eactive.eai.adapter.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; + +import java.lang.reflect.Field; +import java.util.Properties; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.context.ApplicationContext; + +import com.eactive.eai.common.message.EAIMessage; +import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.ApplicationContextProvider; +import com.eactive.eai.message.StandardItem; +import com.eactive.eai.message.StandardMessage; +import com.eactive.eai.message.StandardType; + +/** + * TemplateCodeConvertAdapterErrorMsgHandler 단위테스트 + * + * 검증 대상: generateNonStandardErrorResponseMessage + * - 부모 클래스(TemplateAdapterErrorMsgHandler)가 반환한 JSON 문자열에서 + * PropManager(AdapterErrorMessageHandler{CODE_CONVERT} 그룹)에 등록된 + * 매핑 정보로 지정 필드 값을 변환한다. + */ +class TemplateCodeConvertAdapterErrorMsgHandlerTest { + + private TemplateCodeConvertAdapterErrorMsgHandler handler; + + @BeforeEach + void setUp() { + handler = new TemplateCodeConvertAdapterErrorMsgHandler(); + } + + // ================================================================ + // 공통 헬퍼 + // ================================================================ + + private static StandardItem field(String name, String value) { + return new StandardItem(name, 3, StandardType.FIELD, 1, 0, 300, 1, "", "", value, name); + } + + private static StandardItem group(String name) { + return new StandardItem(name, 2, StandardType.GROUP, 1, 0, 0, 1, "", "", null, name); + } + + private StandardMessage buildMsg(String errCode, String errMsg) { + StandardMessage msg = new StandardMessage(); + StandardItem msgGroup = group("MSG"); + StandardItem mainMsg = group("MAIN_MSG"); + mainMsg.addItem(field("outp_msg_cd", errCode)); + mainMsg.addItem(field("outp_msg_ctnt", errMsg)); + msgGroup.addItem(mainMsg); + + StandardItem msgList = new StandardItem( + "MSG_LIST", 2, StandardType.GRID, 0, 0, 0, 1, "", "", null, "MSG_LIST"); + msgList.addItem(field("outp_msg_cd", "")); + msgList.addItem(field("outp_msg_ctnt", "")); + msgGroup.addItem(msgList); + msg.addItem(msgGroup); + return msg; + } + + private EAIMessage toEaiMessage(StandardMessage msg) { + EAIMessage eaiMessage = new EAIMessage(); + eaiMessage.setStandardMessage(msg); + return eaiMessage; + } + + /** + * ApplicationContextProvider 에 mock PropManager 를 주입한다. + * 부모 클래스의 getProperty(group, key) 와 + * 자식 클래스의 getProperties(group) 을 동일한 mock 으로 처리한다. + */ + private void injectMockPropManager(PropManager mockPropManager) throws Exception { + ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); + Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager); + Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); + ctxField.setAccessible(true); + ctxField.set(null, mockCtx); + } + + /** + * 부모 템플릿 결과가 될 JSON 과 코드 변환 설정을 함께 세팅하는 헬퍼. + * + * @param adapterGroupName 어댑터 그룹명 + * @param template 부모 핸들러에서 사용할 JSON 템플릿 문자열 + * @param convertProps PROP_GROUP(CODE_CONVERT) 에서 반환될 Properties (null 가능) + */ + private PropManager setupMock(String adapterGroupName, String template, + Properties convertProps) throws Exception { + PropManager mockProp = Mockito.mock(PropManager.class); + // 부모 클래스: getProperty("AdapterErrorMessageHandler", "GROUP.template") + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq(adapterGroupName + ".template"))).thenReturn(template); + // 자식 클래스: getProperties("AdapterErrorMessageHandler{CODE_CONVERT}") + Mockito.when(mockProp.getProperties( + eq(TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP))).thenReturn(convertProps); + injectMockPropManager(mockProp); + return mockProp; + } + + /** convert.fields 와 매핑 항목들로 Properties 를 생성하는 헬퍼 */ + private static Properties convertProps(String adapterGroupName, + String fields, + String... mappings) { + Properties p = new Properties(); + p.setProperty(adapterGroupName + ".convert.fields", fields); + for (int i = 0; i < mappings.length - 1; i += 2) { + p.setProperty(adapterGroupName + "." + mappings[i], mappings[i + 1]); + } + return p; + } + + // ================================================================ + // 사전 조건 및 조기 반환 + // ================================================================ + + @Nested + @DisplayName("사전 조건 – 조기 반환") + class EarlyReturn { + + @Test + @DisplayName("부모 핸들러가 null 반환 시 null 그대로 반환") + void 부모_null_반환시_null() throws Exception { + PropManager mockProp = Mockito.mock(PropManager.class); + // 템플릿 없음 → 부모 핸들러가 null 반환 + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); + injectMockPropManager(mockProp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + assertNull(result); + // getProperties 는 호출되지 않아야 함 + Mockito.verify(mockProp, Mockito.never()) + .getProperties(TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP); + } + + @Test + @DisplayName("부모 핸들러가 빈 문자열 반환 시 그대로 반환 (변환 미수행)") + void 부모_빈문자열_그대로_반환() throws Exception { + setupMock("MY_GROUP", " ", null); // blank → 부모도 null 반환 + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + assertNull(result); + } + + @Test + @DisplayName("CODE_CONVERT 그룹의 Properties 가 null 이면 템플릿 결과 그대로 반환") + void convert_props_null_시_원본_반환() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + setupMock("MY_GROUP", template, null); // getProperties → null + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + assertEquals("{\"resCode\":\"40000\"}", result); + } + + @Test + @DisplayName("convert.fields 키가 없으면 템플릿 결과 그대로 반환") + void convert_fields_없으면_원본_반환() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + // convert.fields 를 설정하지 않은 빈 Properties + setupMock("MY_GROUP", template, new Properties()); + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + assertEquals("{\"resCode\":\"40000\"}", result); + } + + @Test + @DisplayName("convert.fields 가 공백이면 템플릿 결과 그대로 반환") + void convert_fields_공백이면_원본_반환() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = new Properties(); + cp.setProperty("MY_GROUP.convert.fields", " "); + setupMock("MY_GROUP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + // 필드가 공백이므로 split 후 trim → blank → skip → 변환 없음 + assertEquals("{\"resCode\":\"40000\"}", result); + } + } + + // ================================================================ + // 단순 필드 코드 변환 + // ================================================================ + + @Nested + @DisplayName("단순 필드 코드 변환") + class SimpleFieldConvert { + + @Test + @DisplayName("단일 필드 값 변환") + void 단일_필드_변환() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "Bad Request"))); + + assertEquals("{\"resCode\":\"TB_ERR_999\",\"resMessage\":\"Bad Request\"}", result); + } + + @Test + @DisplayName("여러 필드 동시 변환") + void 여러_필드_변환() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = convertProps("GRP", + "resCode,resMessage", + "resCode.40000", "TB_ERR_999", + "resMessage.40000", "요청파라미터에 문제가 있습니다.(필수값 입력오류)"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "40000"))); + + assertEquals( + "{\"resCode\":\"TB_ERR_999\",\"resMessage\":\"요청파라미터에 문제가 있습니다.(필수값 입력오류)\"}", + result); + } + + @Test + @DisplayName("매핑에 없는 값은 원본 그대로 유지") + void 매핑_없는_값_원본_유지() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999"); // 99999 에 대한 매핑 없음 + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("99999", "기타오류"))); + + assertEquals("{\"resCode\":\"99999\"}", result); + } + + @Test + @DisplayName("convert.fields 쉼표 구분 중 공백 포함해도 정상 처리") + void fields_공백_포함_정상처리() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = convertProps("GRP", + " resCode , resMessage ", // 공백 포함 + "resCode.40000", "TB_ERR_999", + "resMessage.40000", "필수값 오류"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "40000"))); + + assertEquals("{\"resCode\":\"TB_ERR_999\",\"resMessage\":\"필수값 오류\"}", result); + } + + @Test + @DisplayName("매핑에 없는 값은 default 키로 폴백 변환") + void 매핑_없는_값_default_폴백() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999", + "resCode.default", "TB_ERR_DEFAULT"); // 폴백 + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("99999", ""))); // 99999 매핑 없음 + + assertEquals("{\"resCode\":\"TB_ERR_DEFAULT\"}", result); + } + + @Test + @DisplayName("매핑이 있으면 default 보다 우선 적용") + void 매핑_존재시_default_미사용() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999", + "resCode.default", "TB_ERR_DEFAULT"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", ""))); // 40000 매핑 존재 + + assertEquals("{\"resCode\":\"TB_ERR_999\"}", result); + } + + @Test + @DisplayName("매핑도 default 도 없으면 원본 유지") + void 매핑_default_모두없으면_원본_유지() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999" // 99999 매핑 없음, default 도 없음 + ); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("99999", ""))); + + assertEquals("{\"resCode\":\"99999\"}", result); + } + + @Test + @DisplayName("여러 필드에서 일부는 매핑, 일부는 default 로 변환") + void 여러_필드_매핑과_default_혼합() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = new Properties(); + cp.setProperty("GRP.convert.fields", "resCode,resMessage"); + cp.setProperty("GRP.resCode.40000", "TB_ERR_999"); // 정확 매핑 + cp.setProperty("GRP.resMessage.default", "기타 오류입니다."); // resMessage 는 default 만 + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "Bad Request"))); + + assertEquals("{\"resCode\":\"TB_ERR_999\",\"resMessage\":\"기타 오류입니다.\"}", result); + } + + @Test + @DisplayName("여러 에러코드에 대한 각각 다른 매핑 적용") + void 다른_에러코드_다른_매핑() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = new Properties(); + cp.setProperty("GRP.convert.fields", "resCode"); + cp.setProperty("GRP.resCode.40000", "TB_ERR_999"); + cp.setProperty("GRP.resCode.50000", "TB_ERR_500"); + setupMock("GRP", template, cp); + + // 40000 → TB_ERR_999 + Object result1 = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", ""))); + assertEquals("{\"resCode\":\"TB_ERR_999\"}", result1); + + // 50000 → TB_ERR_500 + Object result2 = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("50000", ""))); + assertEquals("{\"resCode\":\"TB_ERR_500\"}", result2); + } + } + + // ================================================================ + // 중첩 경로(dot path) 필드 코드 변환 + // ================================================================ + + @Nested + @DisplayName("중첩 경로(dot path) 필드 코드 변환") + class NestedPathConvert { + + @Test + @DisplayName("1단계 중첩 경로 필드 변환 – result.code") + void 한단계_중첩_경로_변환() throws Exception { + // 템플릿이 중첩 JSON 을 생성하는 경우 → 변환 전 JSON 을 직접 구성 + // 부모 render 결과를 미리 알고 있으므로, 간단한 템플릿 + 고정값 활용 + String template = "{\"result\":{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}}"; + Properties cp = convertProps("GRP", + "result.code", + "result.code.40000", "TB_ERR_999"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", ""))); + + assertEquals("{\"result\":{\"code\":\"TB_ERR_999\"}}", result); + } + + @Test + @DisplayName("2단계 중첩 경로 필드 변환 – a.b.code") + void 두단계_중첩_경로_변환() throws Exception { + // 부모가 반환할 JSON: 고정 문자열 + stdmsg 치환 없이 직접 지정 + // 어댑터 그룹명 키 + template 이 fixed JSON 을 반환하도록 설정 + String fixedJson = "{\"a\":{\"b\":{\"code\":\"ERR_001\"}}}"; + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("GRP.template"))).thenReturn(fixedJson); // 변수 없는 고정 JSON + Properties cp = convertProps("GRP", + "a.b.code", + "a.b.code.ERR_001", "MAPPED_CODE"); + Mockito.when(mockProp.getProperties( + eq(TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP))).thenReturn(cp); + injectMockPropManager(mockProp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("", ""))); + + assertEquals("{\"a\":{\"b\":{\"code\":\"MAPPED_CODE\"}}}", result); + } + + @Test + @DisplayName("중첩 경로가 존재하지 않으면 해당 필드 변환 건너뜀") + void 없는_중첩_경로_건너뜀() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "nonExist.code", // JSON 에 없는 경로 + "nonExist.code.40000", "MAPPED"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", ""))); + + // 변환 대상 필드가 없으므로 원본 그대로 + assertEquals("{\"resCode\":\"40000\"}", result); + } + } + + // ================================================================ + // 복합 시나리오 + // ================================================================ + + @Nested + @DisplayName("복합 시나리오") + class Combined { + + @Test + @DisplayName("일부 필드만 매핑 존재 – 나머지는 원본 유지") + void 일부_필드_매핑_나머지_원본() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = convertProps("GRP", + "resCode,resMessage", + "resCode.40000", "TB_ERR_999" + // resMessage 매핑 없음 + ); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "Bad Request"))); + + // resCode 는 변환, resMessage 는 원본 유지 + assertEquals("{\"resCode\":\"TB_ERR_999\",\"resMessage\":\"Bad Request\"}", result); + } + + @Test + @DisplayName("프로퍼티 그룹명 검증 – PROP_GROUP 상수 값") + void PROP_GROUP_상수_검증() { + assertEquals("AdapterErrorMessageHandler{CODE_CONVERT}", + TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP); + } + + @Test + @DisplayName("부모 PROP_GROUP 과 자식 PROP_GROUP 이 다른 그룹을 사용") + void 부모_자식_PROP_GROUP_다름() { + // 부모: "AdapterErrorMessageHandler" + // 자식: "AdapterErrorMessageHandler{CODE_CONVERT}" + assertNotEquals( + TemplateAdapterErrorMsgHandler.PROP_GROUP, + TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP); + } + + @Test + @DisplayName("변환 후에도 JSON 에 변환 대상 외 필드는 그대로 유지") + void 변환_후_비대상_필드_유지() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"extra\":\"value\",\"resMessage\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + Properties cp = convertProps("GRP", + "resCode", + "resCode.40000", "TB_ERR_999"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", "오류"))); + + assertEquals("{\"resCode\":\"TB_ERR_999\",\"extra\":\"value\",\"resMessage\":\"오류\"}", result); + } + + @Test + @DisplayName("convert.fields 에 등록된 필드가 JSON 루트에 없어도 예외 없이 처리") + void JSON에_없는_필드_등록시_예외_없음() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = convertProps("GRP", + "resCode,nonExistField", + "resCode.40000", "TB_ERR_999", + "nonExistField.40000", "MAPPED"); + setupMock("GRP", template, cp); + + Object result = handler.generateNonStandardErrorResponseMessage( + "GRP", "ADP", new Properties(), null, + toEaiMessage(buildMsg("40000", ""))); + + // nonExistField 는 getJsonValue 가 null 반환 → 건너뜀, 예외 없음 + assertEquals("{\"resCode\":\"TB_ERR_999\"}", result); + } + + @Test + @DisplayName("inboundAdapterGroupName 이 달라지면 다른 convert.fields 키를 사용") + void 다른_어댑터그룹_다른_키_사용() throws Exception { + String template = "{\"resCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + Properties cp = new Properties(); + cp.setProperty("GROUP_A.convert.fields", "resCode"); + cp.setProperty("GROUP_A.resCode.40000", "A_MAPPED"); + cp.setProperty("GROUP_B.convert.fields", "resCode"); + cp.setProperty("GROUP_B.resCode.40000", "B_MAPPED"); + PropManager mockProp = Mockito.mock(PropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + anyString())).thenReturn(template); + Mockito.when(mockProp.getProperties( + eq(TemplateCodeConvertAdapterErrorMsgHandler.PROP_GROUP))).thenReturn(cp); + injectMockPropManager(mockProp); + + EAIMessage msg = toEaiMessage(buildMsg("40000", "")); + + Object resultA = handler.generateNonStandardErrorResponseMessage( + "GROUP_A", "ADP", new Properties(), null, msg); + Object resultB = handler.generateNonStandardErrorResponseMessage( + "GROUP_B", "ADP", new Properties(), null, msg); + + assertEquals("{\"resCode\":\"A_MAPPED\"}", resultA); + assertEquals("{\"resCode\":\"B_MAPPED\"}", resultB); + } + } + +}