diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java new file mode 100644 index 0000000..2d1956f --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandler.java @@ -0,0 +1,197 @@ +package com.eactive.eai.adapter.handler; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +import com.eactive.eai.adapter.AdapterPropManager; +import com.eactive.eai.common.util.Logger; +import com.eactive.eai.message.StandardItem; +import com.eactive.eai.message.StandardMessage; + +/** + * 템플릿 기반 어댑터 에러 메시지 핸들러. + * + * [프로퍼티 설정] + * AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서 + * "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어 + * StandardMessage 의 필드 값으로 치환한 결과를 반환한다. + * + * [변수 치환 규칙] + * 1. "${callprop.키}" → callProp.getProperty("키") 로 치환 + * 2. "${경로}" → StandardMessage.findItemValue("경로") 로 치환 + * callprop 접두어가 있으면 callProp 을 우선 참조하고, + * 없으면 StandardMessage 경로로 해석한다. + * + * [배열 반복 문법] (MSG_LIST 등 GRID 타입) + * {{#foreach MSG.MSG_LIST}} + * { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" } + * {{/foreach}} + * → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환, + * 반복 결과를 쉼표(,)로 이어 붙임 + * + * [사용 예 – JSON 템플릿] + * { + * "adapterName": "${callprop.ADAPTER_NAME}", + * "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}", + * "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}", + * "errors": [ + * {{#foreach MSG.MSG_LIST}} + * { + * "code": "${outp_msg_cd}", + * "msg": "${outp_msg_ctnt}", + * "desc": "${outp_msg_desc}", + * "field": "${err_occu_item_nm}" + * } + * {{/foreach}} + * ] + * } + * + * [사용 예 – XML 템플릿] + * + * ${callprop.ADAPTER_NAME} + * ${MSG.MAIN_MSG.outp_msg_cd} + * + * {{#foreach MSG.MSG_LIST}} + * ${outp_msg_cd}${outp_msg_ctnt} + * {{/foreach}} + * + * + */ +public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler { + + private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); + + /** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */ + static final String PROP_GROUP = "AdapterErrorMessageHandler"; + + /** callProp 참조 변수 접두어: ${callprop.키} */ + static final String CALLPROP_PREFIX = "callprop."; + + /** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */ + private static final Pattern VAR_PATTERN = + Pattern.compile("\\$\\{([^}]+)\\}"); + + /** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */ + private static final Pattern FOREACH_PATTERN = + Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}", + Pattern.DOTALL); + + @Override + public Object generateNonStandardErrorResponseMessage( + String inboundAdapterGroupName, + String inboundAdapterName, + Properties callProp, + Object outboundRequestData, + StandardMessage resStandardMessage) throws Exception { + + String templateKey = inboundAdapterGroupName + ".template"; + String template = AdapterPropManager.getInstance().getProperty(PROP_GROUP, templateKey); + + if (StringUtils.isBlank(template)) { + if (logger.isWarn()) { + logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP + + ", key=" + templateKey); + } + return null; + } + + return render(template, resStandardMessage, callProp); + } + + /** + * 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다. + * package-private: 단위 테스트에서 직접 호출 가능 + */ + String render(String template, StandardMessage msg, Properties callProp) { + // 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리 + StringBuffer sb = new StringBuffer(); + Matcher foreachMatcher = FOREACH_PATTERN.matcher(template); + while (foreachMatcher.find()) { + String arrayPath = foreachMatcher.group(1).trim(); + String blockContent = foreachMatcher.group(2); + String rendered = renderForeach(arrayPath, blockContent, msg); + foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered)); + } + foreachMatcher.appendTail(sb); + + // 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환 + String intermediate = sb.toString(); + StringBuffer result = new StringBuffer(); + Matcher varMatcher = VAR_PATTERN.matcher(intermediate); + while (varMatcher.find()) { + String expr = varMatcher.group(1).trim(); + String value = resolveScalar(expr, msg, callProp); + varMatcher.appendReplacement(result, Matcher.quoteReplacement(value)); + } + varMatcher.appendTail(result); + return result.toString(); + } + + /** + * 변수 표현식을 해석해 값을 반환한다. + * - "callprop.키" → callProp.getProperty("키") + * - 그 외 → StandardMessage.findItemValue(expr) + */ + private String resolveScalar(String expr, StandardMessage msg, Properties callProp) { + if (expr.startsWith(CALLPROP_PREFIX)) { + String key = expr.substring(CALLPROP_PREFIX.length()); + if (callProp != null) { + return StringUtils.defaultString(callProp.getProperty(key)); + } + return ""; + } + return StringUtils.defaultString(msg.findItemValue(expr)); + } + + /** + * GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환, + * 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원) + */ + private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) { + StandardItem arrayItem = msg.findItem(arrayPath); + if (arrayItem == null) { + if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath); + return ""; + } + + List> rows = arrayItem.getList(); + if (rows == null || rows.isEmpty()) { + return ""; + } + + List renderedItems = new ArrayList<>(); + for (LinkedHashMap row : rows) { + String rowText = renderRow(blockContent, row); + if (StringUtils.isNotBlank(rowText)) { + renderedItems.add(rowText); + } + } + return String.join(",", renderedItems); + } + + /** + * 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다. + * fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다. + */ + private String renderRow(String blockContent, LinkedHashMap row) { + StringBuffer result = new StringBuffer(); + Matcher varMatcher = VAR_PATTERN.matcher(blockContent); + while (varMatcher.find()) { + String fieldName = varMatcher.group(1).trim(); + String value = ""; + StandardItem item = row.get(fieldName); + if (item != null) { + value = StringUtils.defaultString(item.getValue()); + } + varMatcher.appendReplacement(result, Matcher.quoteReplacement(value)); + } + varMatcher.appendTail(result); + return result.toString().trim(); + } +} diff --git a/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java new file mode 100644 index 0000000..7326022 --- /dev/null +++ b/src/test/java/com/eactive/eai/adapter/handler/TemplateAdapterErrorMsgHandlerTest.java @@ -0,0 +1,439 @@ +package com.eactive.eai.adapter.handler; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; + +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +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.adapter.AdapterPropManager; +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; + +/** + * TemplateAdapterErrorMsgHandler 단위테스트 + * + * - render() : package-private 이므로 같은 패키지에서 직접 호출 + * - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에 + * mock ApplicationContext 를 리플렉션으로 주입하여 AdapterPropManager 격리 + */ +class TemplateAdapterErrorMsgHandlerTest { + + private TemplateAdapterErrorMsgHandler handler; + + @BeforeEach + void setUp() { + handler = new TemplateAdapterErrorMsgHandler(); + } + + // ================================================================ + // StandardMessage 빌더 헬퍼 + // ================================================================ + + /** FIELD 아이템 (type=2) */ + private static StandardItem field(String name, String value) { + return new StandardItem(name, 3, StandardType.FIELD, 1, 0, 300, 1, "", "", value, name); + } + + /** GROUP 아이템 (type=3, size=1 → 활성) */ + private static StandardItem group(String name) { + return new StandardItem(name, 2, StandardType.GROUP, 1, 0, 0, 1, "", "", null, name); + } + + /** + * DJB 표준전문 MSG 영역을 모사한 StandardMessage 생성. + * + * 구조: + * MSG (GROUP) + * MAIN_MSG (GROUP) + * outp_msg_cd, outp_msg_ctnt, outp_msg_desc + * MSG_LIST (GRID) + * row[n]: outp_msg_cd, outp_msg_ctnt, outp_msg_desc, err_occu_item_nm + * + * @param listRows null 또는 빈 배열이면 MSG_LIST 행 없음 + */ + private StandardMessage buildMsg(String errCode, String errMsg, String errDesc, + String[][] listRows) { + 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)); + mainMsg.addItem(field("outp_msg_desc", errDesc)); + 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", "")); + msgList.addItem(field("outp_msg_desc", "")); + msgList.addItem(field("err_occu_item_nm", "")); + + if (listRows != null) { + for (String[] row : listRows) { + LinkedHashMap rowMap = new LinkedHashMap<>(); + rowMap.put("outp_msg_cd", field("outp_msg_cd", get(row, 0))); + rowMap.put("outp_msg_ctnt", field("outp_msg_ctnt", get(row, 1))); + rowMap.put("outp_msg_desc", field("outp_msg_desc", get(row, 2))); + rowMap.put("err_occu_item_nm", field("err_occu_item_nm", get(row, 3))); + msgList.addArray(rowMap); + } + } + msgGroup.addItem(msgList); + msg.addItem(msgGroup); + return msg; + } + + private static String get(String[] arr, int idx) { + return (arr != null && arr.length > idx) ? arr[idx] : ""; + } + + private static Properties props(String... keyValues) { + Properties p = new Properties(); + for (int i = 0; i < keyValues.length - 1; i += 2) { + p.setProperty(keyValues[i], keyValues[i + 1]); + } + return p; + } + + // ================================================================ + // 스칼라 변수 치환 ${path} + // ================================================================ + + @Nested + @DisplayName("StandardMessage 스칼라 변수 치환 ${path}") + class ScalarSubstitution { + + @Test + @DisplayName("단순 JSON 템플릿 치환") + void 단순_치환() { + StandardMessage msg = buildMsg("E001", "오류발생", "상세설명", null); + String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}"; + + String result = handler.render(template, msg, null); + + assertEquals("{\"code\":\"E001\",\"msg\":\"오류발생\"}", result); + } + + @Test + @DisplayName("동일 변수 중복 치환") + void 동일_변수_중복_치환() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + String template = "${MSG.MAIN_MSG.outp_msg_cd}/${MSG.MAIN_MSG.outp_msg_cd}"; + + assertEquals("E001/E001", handler.render(template, msg, null)); + } + + @Test + @DisplayName("존재하지 않는 경로는 빈 문자열로 치환") + void 없는_경로_빈_문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("", handler.render("${NONE.PATH}", msg, null)); + } + + @Test + @DisplayName("변수 없는 템플릿은 원본 그대로 반환") + void 변수_없는_템플릿_원본() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("{\"static\":\"value\"}", handler.render("{\"static\":\"value\"}", msg, null)); + } + + @Test + @DisplayName("outp_msg_desc 필드 치환") + void outp_msg_desc_치환() { + StandardMessage msg = buildMsg("E001", "오류", "상세설명입니다", null); + + assertEquals("상세설명입니다", handler.render("${MSG.MAIN_MSG.outp_msg_desc}", msg, null)); + } + } + + // ================================================================ + // callProp 변수 치환 ${callprop.키} + // ================================================================ + + @Nested + @DisplayName("callProp 변수 치환 ${callprop.키}") + class CallPropSubstitution { + + @Test + @DisplayName("callProp 값을 템플릿에서 참조") + void callProp_단순_참조() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001"); + String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}"; + + String result = handler.render(template, msg, callProp); + + assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result); + } + + @Test + @DisplayName("callProp 과 StandardMessage 혼합 참조") + void callProp_stdmsg_혼합() { + StandardMessage msg = buildMsg("E001", "오류발생", "", null); + Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER"); + String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + + String result = handler.render(template, msg, callProp); + + assertEquals("{\"adapter\":\"MY_ADAPTER\",\"code\":\"E001\"}", result); + } + + @Test + @DisplayName("callProp 에 없는 키는 빈 문자열") + void callProp_없는_키_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + Properties callProp = new Properties(); + + assertEquals("", handler.render("${callprop.NONE_KEY}", msg, callProp)); + } + + @Test + @DisplayName("callProp 이 null 이면 빈 문자열") + void callProp_null_빈문자열() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + + assertEquals("", handler.render("${callprop.ADAPTER_NAME}", msg, null)); + } + } + + // ================================================================ + // 배열 반복 {{#foreach path}}...{{/foreach}} + // ================================================================ + + @Nested + @DisplayName("배열 반복 블록 {{#foreach}}") + class ForeachBlock { + + @Test + @DisplayName("MSG_LIST 1건 렌더링") + void 단건_렌더링() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{{"E001", "메시지A", "설명A", "fieldA"}}); + String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}{{/foreach}}]"; + + assertEquals("[{\"c\":\"E001\",\"m\":\"메시지A\"}]", handler.render(template, msg, null)); + } + + @Test + @DisplayName("MSG_LIST 2건 → 쉼표로 연결") + void 다건_쉼표_연결() { + StandardMessage msg = buildMsg("E001", "대표오류", "", + new String[][]{ + {"E001", "오류A", "설명A", "fieldA"}, + {"E002", "오류B", "설명B", "fieldB"} + }); + String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]"; + + assertEquals("[{\"c\":\"E001\"},{\"c\":\"E002\"}]", handler.render(template, msg, null)); + } + + @Test + @DisplayName("MSG_LIST 가 비어있으면 foreach 결과는 빈 문자열") + void 빈_배열() { + StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{}); + String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]"; + + assertEquals("[]", handler.render(template, msg, null)); + } + + @Test + @DisplayName("foreach 경로가 없으면 빈 문자열") + void 없는_경로() { + StandardMessage msg = buildMsg("E001", "오류", "", null); + String template = "[{{#foreach NONE.LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]"; + + assertEquals("[]", handler.render(template, msg, null)); + } + + @Test + @DisplayName("err_occu_item_nm 필드 포함 렌더링") + void err_occu_item_nm_포함() { + StandardMessage msg = buildMsg("E001", "오류", "", + new String[][]{{"E001", "오류A", "설명A", "accountNo"}}); + String template = "{{#foreach MSG.MSG_LIST}}{\"field\":\"${err_occu_item_nm}\"}{{/foreach}}"; + + assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null)); + } + } + + // ================================================================ + // 혼합 템플릿 (스칼라 + callProp + 배열) + // ================================================================ + + @Nested + @DisplayName("혼합 템플릿") + class MixedTemplate { + + @Test + @DisplayName("JSON – MAIN_MSG + callProp + MSG_LIST 2건") + void JSON_전체_혼합() { + StandardMessage msg = buildMsg("E001", "대표오류", "대표설명", + new String[][]{ + {"E001", "오류A", "설명A", "fieldA"}, + {"E002", "오류B", "설명B", "fieldB"} + }); + Properties callProp = props("ADAPTER_NAME", "TEST_ADAPTER"); + + String template = + "{" + + "\"adapter\":\"${callprop.ADAPTER_NAME}\"," + + "\"resultCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," + + "\"resultMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"," + + "\"errors\":[{{#foreach MSG.MSG_LIST}}" + + "{\"code\":\"${outp_msg_cd}\",\"msg\":\"${outp_msg_ctnt}\",\"field\":\"${err_occu_item_nm}\"}" + + "{{/foreach}}]" + + "}"; + + String result = handler.render(template, msg, callProp); + + assertEquals( + "{\"adapter\":\"TEST_ADAPTER\"," + + "\"resultCode\":\"E001\",\"resultMsg\":\"대표오류\"," + + "\"errors\":[{\"code\":\"E001\",\"msg\":\"오류A\",\"field\":\"fieldA\"}," + + "{\"code\":\"E002\",\"msg\":\"오류B\",\"field\":\"fieldB\"}]}", + result); + } + + @Test + @DisplayName("XML – 스칼라 + callProp 치환") + void XML_스칼라_callProp() { + StandardMessage msg = buildMsg("E001", "오류발생", "", null); + Properties callProp = props("IF_ID", "IF_001"); + String template = "${callprop.IF_ID}${MSG.MAIN_MSG.outp_msg_cd}"; + + assertEquals("IF_001E001", + handler.render(template, msg, callProp)); + } + + @Test + @DisplayName("XML – foreach 배열") + void XML_foreach_배열() { + StandardMessage msg = buildMsg("E001", "오류", "", + new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}}); + String template = + "{{#foreach MSG.MSG_LIST}}" + + "${outp_msg_cd}${outp_msg_ctnt}" + + "{{/foreach}}"; + + assertEquals( + "" + + "E001오류A," + + "E002오류B" + + "", + handler.render(template, msg, null)); + } + } + + // ================================================================ + // generateNonStandardErrorResponseMessage – AdapterPropManager mock + // ================================================================ + + @Nested + @DisplayName("generateNonStandardErrorResponseMessage") + class GenerateMethod { + + private void injectMockPropManager(AdapterPropManager mockPropManager) throws Exception { + ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class); + Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockPropManager); + Field ctxField = ApplicationContextProvider.class.getDeclaredField("context"); + ctxField.setAccessible(true); + ctxField.set(null, mockCtx); + } + + @Test + @DisplayName("템플릿 미설정 시 null 반환") + void 템플릿_없으면_null() throws Exception { + AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); + injectMockPropManager(mockProp); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + Object result = handler.generateNonStandardErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg); + + assertNull(result); + } + + @Test + @DisplayName("빈 템플릿 시 null 반환") + void 빈_템플릿_null() throws Exception { + AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" "); + injectMockPropManager(mockProp); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + Object result = handler.generateNonStandardErrorResponseMessage( + "TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg); + + assertNull(result); + } + + @Test + @DisplayName("템플릿 설정 시 StandardMessage 치환 결과 반환") + void 템플릿_stdmsg_치환() throws Exception { + String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("MY_GROUP.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + StandardMessage msg = buildMsg("E999", "테스트오류", "", null); + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", new Properties(), null, msg); + + assertEquals("{\"code\":\"E999\"}", result); + } + + @Test + @DisplayName("callProp 값이 템플릿에 치환되어 반환") + void 템플릿_callProp_치환() throws Exception { + String template = "{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}"; + AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + Mockito.when(mockProp.getProperty( + eq(TemplateAdapterErrorMsgHandler.PROP_GROUP), + eq("MY_GROUP.template"))).thenReturn(template); + injectMockPropManager(mockProp); + + StandardMessage msg = buildMsg("E999", "테스트오류", "", null); + Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER"); + + Object result = handler.generateNonStandardErrorResponseMessage( + "MY_GROUP", "MY_ADAPTER", callProp, null, msg); + + assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result); + } + + @Test + @DisplayName("템플릿 키는 {adapterGroupId}.template 형식으로 조회") + void 템플릿_키_형식_검증() throws Exception { + AdapterPropManager mockProp = Mockito.mock(AdapterPropManager.class); + Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null); + injectMockPropManager(mockProp); + + StandardMessage msg = buildMsg("E001", "오류", "", null); + handler.generateNonStandardErrorResponseMessage( + "SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg); + + Mockito.verify(mockProp).getProperty( + TemplateAdapterErrorMsgHandler.PROP_GROUP, + "SAMPLE_GROUP.template"); + } + } +}