TemplateAdapterErrorMsgHandler 구현 작업 내역

-인바운드/아웃바운드/내부 오류 발생 시 어댑터별로 프로퍼티에 등록된 템플릿을 사용하여
 커스텀 에러 응답 메시지를 생성하는 TemplateAdapterErrorMsgHandler 구현 및 연동 작업.
- ExceptionHandler 변경
인바운드 어댑터에 ERR_MSG_HANDLER 프로퍼티가 설정된 경우 우선 실행
- ApiController에서 사용할 수 있는
generateNonStandardInboundErrorResponseMessage 추가, Controller에서도
Adapter가 식별되면 설정된 핸들러에 의해 에러메시지 행성 가능
This commit is contained in:
curry772
2026-06-09 13:47:25 +09:00
parent d15b2d563e
commit a538b9838e
9 changed files with 1037 additions and 95 deletions
@@ -1,6 +1,8 @@
package com.eactive.eai.adapter.handler;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -12,9 +14,17 @@ 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.InOrder;
import org.mockito.Mockito;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.adapter.AdapterGroupVO;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
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;
@@ -25,7 +35,7 @@ import com.eactive.eai.message.StandardType;
* TemplateAdapterErrorMsgHandler 단위테스트
*
* - render() : package-private 이므로 같은 패키지에서 직접 호출
* - generateNonStandardErrorResponseMessage() : ApplicationContextProvider 에
* - generateNonStandard*() : ApplicationContextProvider 에
* mock ApplicationContext 를 리플렉션으로 주입하여 PropManager 격리
*/
class TemplateAdapterErrorMsgHandlerTest {
@@ -38,7 +48,7 @@ class TemplateAdapterErrorMsgHandlerTest {
}
// ================================================================
// StandardMessage 빌더 헬퍼
// 공통 헬퍼
// ================================================================
/** FIELD 아이템 (type=2) */
@@ -109,6 +119,22 @@ class TemplateAdapterErrorMsgHandlerTest {
return p;
}
/** ApplicationContextProvider 에 mock PropManager 를 주입한다. */
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);
}
/** EAIMessage 에 StandardMessage 를 주입해 반환한다. */
private EAIMessage toEaiMessage(StandardMessage msg) {
EAIMessage eaiMessage = new EAIMessage();
eaiMessage.setStandardMessage(msg);
return eaiMessage;
}
// ================================================================
// 스칼라 변수 치환 ${path}
// ================================================================
@@ -275,12 +301,11 @@ class TemplateAdapterErrorMsgHandlerTest {
@DisplayName("중간 값이 Properties 가 아니면 전체 keyPath 로 폴백 조회")
void callProp_중간값_비Properties_폴백() {
Properties callProp = new Properties();
callProp.setProperty("A.B", "FALLBACK_VALUE"); // 점 포함 키
callProp.setProperty("A", "NOT_PROPS"); // String 값
callProp.setProperty("A.B", "FALLBACK_VALUE");
callProp.setProperty("A", "NOT_PROPS");
StandardMessage msg = buildMsg("E001", "오류", "", null);
// A 는 String 이므로 Properties 가 아님 → "A.B" 전체 키로 폴백
assertEquals("FALLBACK_VALUE", handler.render("${callprop.A.B}", msg, callProp));
}
@@ -333,10 +358,8 @@ class TemplateAdapterErrorMsgHandlerTest {
@DisplayName("callProp 간접 참조 ${callprop[경로]}")
class IndirectCallPropSubstitution {
/** 표준전문에 cp_key 필드가 있는 메시지 */
private StandardMessage buildMsgWithKey(String cpKey, String errCode) {
StandardMessage msg = buildMsg(errCode, "오류", "", null);
// MSG 그룹 아래 cp_key 필드 추가
StandardItem msgGroup = msg.findItem("MSG");
if (msgGroup != null) {
msgGroup.addItem(field("cp_key", cpKey));
@@ -414,10 +437,6 @@ class TemplateAdapterErrorMsgHandlerTest {
}
}
// ================================================================
// 배열 반복 {{#foreach path}}...{{/foreach}}
// ================================================================
// ================================================================
// DATA 영역 변수 치환 ${DATA.xxx}
// ================================================================
@@ -426,17 +445,11 @@ class TemplateAdapterErrorMsgHandlerTest {
@DisplayName("DATA 영역 변수 치환 ${DATA.xxx}")
class DataSectionSubstitution {
/**
* DATA > BIZDATA(FIELD, JSON String value) 구조의 StandardMessage 생성.
* BIZDATA 는 파싱 없이 JSON 문자열 그대로 세팅되는 형태.
*/
private StandardMessage buildMsgWithBizData(String bizDataJson) {
StandardMessage msg = buildMsg("E001", "오류", "", null);
StandardItem dataGroup = group("DATA");
dataGroup.addItem(field("BIZDATA", bizDataJson));
msg.addItem(dataGroup);
return msg;
}
@@ -502,6 +515,10 @@ class TemplateAdapterErrorMsgHandlerTest {
}
}
// ================================================================
// 배열 반복 {{#foreach path}}...{{/foreach}}
// ================================================================
@Nested
@DisplayName("배열 반복 블록 {{#foreach}}")
class ForeachBlock {
@@ -538,6 +555,17 @@ class TemplateAdapterErrorMsgHandlerTest {
assertEquals("[]", handler.render(template, msg, null));
}
@Test
@DisplayName("# 이 HTML 인코딩된 foreach 도 정상 처리")
void HTML_인코딩된_foreach() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{{"E001", "오류A", "", ""}});
// 관리 UI 가 # 을 # 로 저장한 경우
String template = "[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]";
assertEquals("[{\"c\":\"E001\"}]", handler.render(template, msg, null));
}
@Test
@DisplayName("foreach 경로가 없으면 빈 문자열")
void 없는_경로() {
@@ -556,6 +584,266 @@ class TemplateAdapterErrorMsgHandlerTest {
assertEquals("{\"field\":\"accountNo\"}", handler.render(template, msg, null));
}
@Test
@DisplayName("스칼라 → foreach → 스칼라 순서 처리 – 단일 패스 검증 (사용자 템플릿 형식)")
void 스칼라_foreach_스칼라_단일패스() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{{"E001", "오류A", "설명A", "fieldA"}});
// 스칼라 앞, foreach 중간, 스칼라 뒤 순서로 배치
String template =
"{\"mainCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"{{#foreach MSG.MSG_LIST}}{\"code\":\"${outp_msg_cd}\"}{{/foreach}}," +
"\"mainMsg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
String result = handler.render(template, msg, null);
assertEquals(
"{\"mainCode\":\"E001\"," +
"{\"code\":\"E001\"}," +
"\"mainMsg\":\"대표오류\"}",
result);
}
@Test
@DisplayName("foreach 앞뒤 스칼라 + 2건 리스트 – 단일 패스 검증")
void 스칼라_foreach_스칼라_다건() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{
{"E001", "오류A", "", ""},
{"E002", "오류B", "", ""}
});
String template =
"{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"\"errors\":[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]," +
"\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
String result = handler.render(template, msg, null);
assertEquals(
"{\"code\":\"E001\"," +
"\"errors\":[{\"c\":\"E001\"},{\"c\":\"E002\"}]," +
"\"msg\":\"대표오류\"}",
result);
}
}
// ================================================================
// 예외 객체 필드 치환 ${exception.필드명}
// ================================================================
@Nested
@DisplayName("예외 객체 필드 치환 ${exception.필드명}")
class ExceptionSubstitution {
@Test
@DisplayName("exception.message → getMessage() 반환")
void exception_message() {
StandardMessage msg = buildMsg("E001", "오류", "", null);
RuntimeException e = new RuntimeException("처리 중 오류 발생");
String result = handler.render("${exception.message}", msg, null, e);
assertEquals("처리 중 오류 발생", result);
}
@Test
@DisplayName("HttpStatusException.status → getStatus() 반환")
void HttpStatusException_status() {
HttpStatusException e = new HttpStatusException("Bad Request", 400);
assertEquals("400", handler.render("${exception.status}", null, null, e));
}
@Test
@DisplayName("HttpStatusException.code → getCode() 반환")
void HttpStatusException_code() {
HttpStatusException e = new HttpStatusException("오류", "ERR_CODE", 400);
assertEquals("ERR_CODE", handler.render("${exception.code}", null, null, e));
}
@Test
@DisplayName("JwtAuthException.code → getCode() 반환")
void JwtAuthException_code() {
JwtAuthException e = new JwtAuthException("JWT_EXPIRED", "토큰 만료");
assertEquals("JWT_EXPIRED", handler.render("${exception.code}", null, null, e));
}
@Test
@DisplayName("JwtAuthException.message → getMessage() 반환")
void JwtAuthException_message() {
JwtAuthException e = new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다");
assertEquals("토큰이 만료되었습니다", handler.render("${exception.message}", null, null, e));
}
@Test
@DisplayName("존재하지 않는 필드는 빈 문자열")
void 없는_필드_빈문자열() {
RuntimeException e = new RuntimeException("오류");
assertEquals("", handler.render("${exception.noneField}", null, null, e));
}
@Test
@DisplayName("존재하지 않는 필드에 기본값 지정")
void 없는_필드_기본값() {
RuntimeException e = new RuntimeException("오류");
assertEquals("UNKNOWN", handler.render("${exception.noneField:UNKNOWN}", null, null, e));
}
@Test
@DisplayName("exception 이 null 이면 기본값 반환")
void exception_null_기본값() {
assertEquals("DEFAULT", handler.render("${exception.message:DEFAULT}", null, null, null));
}
@Test
@DisplayName("exception + callProp + StandardMessage 혼합 치환")
void exception_callProp_stdmsg_혼합() {
StandardMessage msg = buildMsg("E001", "대표오류", "", null);
Properties callProp = props("IF_ID", "SVC001");
HttpStatusException e = new HttpStatusException("Bad Request", 400);
String template =
"{\"ifId\":\"${callprop.IF_ID}\"," +
"\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"\"httpStatus\":\"${exception.status}\"," +
"\"errMsg\":\"${exception.message}\"}";
String result = handler.render(template, msg, callProp, e);
assertEquals(
"{\"ifId\":\"SVC001\"," +
"\"code\":\"E001\"," +
"\"httpStatus\":\"400\"," +
"\"errMsg\":\"Bad Request\"}",
result);
}
@Test
@DisplayName("부모 클래스(Throwable) 필드도 리플렉션으로 접근")
void 부모클래스_필드_접근() {
// Throwable 의 detailMessage 필드 (getMessage() 로 접근됨)
HttpStatusException e = new HttpStatusException("서비스 불가", 503);
// getStatus 는 HttpStatusException 직접 정의 메서드
assertEquals("503", handler.render("${exception.status}", null, null, e));
// getMessage 는 Throwable 상속 메서드
assertEquals("서비스 불가", handler.render("${exception.message}", null, null, e));
}
}
// ================================================================
// 배열 인덱스 직접 접근 ${path[n].field}
// ================================================================
@Nested
@DisplayName("배열 인덱스 직접 접근 ${path[n].field}")
class ArrayIndexSubstitution {
@Test
@DisplayName("첫 번째 행(인덱스 0) 필드 접근")
void 첫번째_행_필드_접근() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{
{"E001", "오류A", "설명A", "fieldA"},
{"E002", "오류B", "설명B", "fieldB"}
});
assertEquals("E001", handler.render("${MSG.MSG_LIST[0].outp_msg_cd}", msg, null));
}
@Test
@DisplayName("두 번째 행(인덱스 1) 필드 접근")
void 두번째_행_필드_접근() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{
{"E001", "오류A", "설명A", "fieldA"},
{"E002", "오류B", "설명B", "fieldB"}
});
assertEquals("E002", handler.render("${MSG.MSG_LIST[1].outp_msg_cd}", msg, null));
}
@Test
@DisplayName("여러 인덱스를 한 템플릿에서 동시 참조")
void 여러_인덱스_동시_참조() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{
{"E001", "오류A", "", ""},
{"E002", "오류B", "", ""}
});
String template = "{\"first\":\"${MSG.MSG_LIST[0].outp_msg_cd}\",\"second\":\"${MSG.MSG_LIST[1].outp_msg_cd}\"}";
assertEquals("{\"first\":\"E001\",\"second\":\"E002\"}", handler.render(template, msg, null));
}
@Test
@DisplayName("인덱스가 범위를 벗어나면 기본값 반환")
void 인덱스_범위_초과_기본값() {
StandardMessage msg = buildMsg("E001", "오류", "",
new String[][]{{"E001", "오류A", "", ""}});
assertEquals("NONE", handler.render("${MSG.MSG_LIST[5].outp_msg_cd:NONE}", msg, null));
}
@Test
@DisplayName("인덱스가 범위를 벗어나면 빈 문자열 (기본값 미지정)")
void 인덱스_범위_초과_빈문자열() {
StandardMessage msg = buildMsg("E001", "오류", "",
new String[][]{{"E001", "오류A", "", ""}});
assertEquals("", handler.render("${MSG.MSG_LIST[9].outp_msg_cd}", msg, null));
}
@Test
@DisplayName("MSG_LIST 가 비어있을 때 인덱스 접근은 빈 문자열")
void 빈_배열_인덱스_접근() {
StandardMessage msg = buildMsg("E001", "오류", "", new String[][]{});
assertEquals("", handler.render("${MSG.MSG_LIST[0].outp_msg_cd}", msg, null));
}
@Test
@DisplayName("배열 경로가 없으면 기본값 반환")
void 없는_배열_경로() {
StandardMessage msg = buildMsg("E001", "오류", "", null);
assertEquals("DEFAULT", handler.render("${NONE.LIST[0].outp_msg_cd:DEFAULT}", msg, null));
}
@Test
@DisplayName("err_occu_item_nm 필드 인덱스 접근")
void err_occu_item_nm_인덱스_접근() {
StandardMessage msg = buildMsg("E001", "오류", "",
new String[][]{
{"E001", "오류A", "", "accountNo"},
{"E002", "오류B", "", "amount"}
});
assertEquals("accountNo", handler.render("${MSG.MSG_LIST[0].err_occu_item_nm}", msg, null));
assertEquals("amount", handler.render("${MSG.MSG_LIST[1].err_occu_item_nm}", msg, null));
}
@Test
@DisplayName("인덱스 접근과 MAIN_MSG 스칼라 혼합")
void 인덱스_접근_스칼라_혼합() {
StandardMessage msg = buildMsg("E001", "대표오류", "",
new String[][]{{"E001", "오류A", "", "fieldA"}});
String template =
"{\"mainCode\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"\"firstCode\":\"${MSG.MSG_LIST[0].outp_msg_cd}\"," +
"\"firstField\":\"${MSG.MSG_LIST[0].err_occu_item_nm}\"}";
assertEquals(
"{\"mainCode\":\"E001\"," +
"\"firstCode\":\"E001\"," +
"\"firstField\":\"fieldA\"}",
handler.render(template, msg, null));
}
}
// ================================================================
@@ -627,21 +915,14 @@ class TemplateAdapterErrorMsgHandlerTest {
}
// ================================================================
// generateNonStandardErrorResponseMessage PropManager mock
// generateNonStandardErrorResponseMessage
// 템플릿 키: {adapterGroupId}.template
// ================================================================
@Nested
@DisplayName("generateNonStandardErrorResponseMessage")
class GenerateMethod {
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);
}
@Test
@DisplayName("템플릿 미설정 시 null 반환")
void 템플릿_없으면_null() throws Exception {
@@ -649,9 +930,9 @@ class TemplateAdapterErrorMsgHandlerTest {
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
injectMockPropManager(mockProp);
StandardMessage msg = buildMsg("E001", "오류", "", null);
EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null));
Object result = handler.generateNonStandardErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, resEaiMsg);
assertNull(result);
}
@@ -663,9 +944,9 @@ class TemplateAdapterErrorMsgHandlerTest {
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" ");
injectMockPropManager(mockProp);
StandardMessage msg = buildMsg("E001", "오류", "", null);
EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null));
Object result = handler.generateNonStandardErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, msg);
"TEST_GROUP", "TEST_ADAPTER", new Properties(), null, resEaiMsg);
assertNull(result);
}
@@ -680,9 +961,9 @@ class TemplateAdapterErrorMsgHandlerTest {
eq("MY_GROUP.template"))).thenReturn(template);
injectMockPropManager(mockProp);
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
EAIMessage resEaiMsg = toEaiMessage(buildMsg("E999", "테스트오류", "", null));
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", new Properties(), null, msg);
"MY_GROUP", "MY_ADAPTER", new Properties(), null, resEaiMsg);
assertEquals("{\"code\":\"E999\"}", result);
}
@@ -697,11 +978,10 @@ class TemplateAdapterErrorMsgHandlerTest {
eq("MY_GROUP.template"))).thenReturn(template);
injectMockPropManager(mockProp);
StandardMessage msg = buildMsg("E999", "테스트오류", "", null);
EAIMessage resEaiMsg = toEaiMessage(buildMsg("E999", "테스트오류", "", null));
Properties callProp = props("ADAPTER_NAME", "REAL_ADAPTER");
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", callProp, null, msg);
"MY_GROUP", "MY_ADAPTER", callProp, null, resEaiMsg);
assertEquals("{\"adapter\":\"REAL_ADAPTER\",\"code\":\"E999\"}", result);
}
@@ -713,13 +993,394 @@ class TemplateAdapterErrorMsgHandlerTest {
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
injectMockPropManager(mockProp);
StandardMessage msg = buildMsg("E001", "오류", "", null);
EAIMessage resEaiMsg = toEaiMessage(buildMsg("E001", "오류", "", null));
handler.generateNonStandardErrorResponseMessage(
"SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, msg);
"SAMPLE_GROUP", "ANY_ADAPTER", new Properties(), null, resEaiMsg);
Mockito.verify(mockProp).getProperty(
TemplateAdapterErrorMsgHandler.PROP_GROUP,
"SAMPLE_GROUP.template");
}
}
// ================================================================
// generateNonStandardInternalErrorResponseMessage (ExceptionHandler 내부 오류용)
// 템플릿 키: {adapterGroupId}.sys.template
// ================================================================
@Nested
@DisplayName("generateNonStandardInternalErrorResponseMessage")
class GenerateInternalMethod {
@Test
@DisplayName("템플릿 미설정 시 null 반환")
void 템플릿_없으면_null() throws Exception {
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, eaiMessage);
assertNull(result);
}
@Test
@DisplayName("빈 템플릿 시 null 반환")
void 빈_템플릿_null() throws Exception {
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(" ");
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, eaiMessage);
assertNull(result);
}
@Test
@DisplayName("템플릿 설정 시 EAIMessage 의 StandardMessage 로 치환")
void 템플릿_stdmsg_치환() throws Exception {
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("MY_GROUP.sys.template"))).thenReturn(template);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E999", "내부오류", "", null));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null, eaiMessage);
assertEquals("{\"code\":\"E999\",\"msg\":\"내부오류\"}", result);
}
@Test
@DisplayName("callProp 이 null 이어도 스칼라 기본값 처리 (ExceptionHandler 에서 null 전달)")
void callProp_null_기본값_처리() throws Exception {
String template = "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"adapter\":\"${callprop.ADAPTER_NAME:UNKNOWN}\"}";
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("MY_GROUP.sys.template"))).thenReturn(template);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E500", "시스템오류", "", null));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null, eaiMessage);
// callProp null → 기본값 UNKNOWN 반환
assertEquals("{\"code\":\"E500\",\"adapter\":\"UNKNOWN\"}", result);
}
@Test
@DisplayName("MSG_LIST foreach 포함 템플릿 치환")
void foreach_MSG_LIST_포함() throws Exception {
String template =
"{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"\"errors\":[{{#foreach MSG.MSG_LIST}}" +
"{\"c\":\"${outp_msg_cd}\",\"m\":\"${outp_msg_ctnt}\"}" +
"{{/foreach}}]}";
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("GW_GROUP.sys.template"))).thenReturn(template);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "대표오류", "",
new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}}));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"GW_GROUP", "GW_ADAPTER", null, null, eaiMessage);
assertEquals(
"{\"code\":\"E001\"," +
"\"errors\":[{\"c\":\"E001\",\"m\":\"오류A\"}," +
"{\"c\":\"E002\",\"m\":\"오류B\"}]}",
result);
}
@Test
@DisplayName("템플릿 키는 {adapterGroupId}.sys.template 형식으로 조회")
void 템플릿_키_형식_검증() throws Exception {
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(anyString(), anyString())).thenReturn(null);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류", "", null));
handler.generateNonStandardInternalErrorResponseMessage(
"SAMPLE_GROUP", "ANY_ADAPTER", null, null, eaiMessage);
Mockito.verify(mockProp).getProperty(
TemplateAdapterErrorMsgHandler.PROP_GROUP,
"SAMPLE_GROUP.sys.template");
}
@Test
@DisplayName("스칼라 → foreach → 스칼라 순서 – 사용자 실제 템플릿 형식")
void 사용자_실제_템플릿_형식() throws Exception {
// ExceptionHandler 에서 사용되는 실제 템플릿 패턴: 스칼라, foreach, 스칼라 순서
String template =
"{\"aeh-sys-resultCode1\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"{{#foreach MSG.MSG_LIST}}" +
"\"aeh-sys-resultCode1\":\"${outp_msg_cd}\"" +
"{{/foreach}}," +
"\"aeh-sys-resultMsg1\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}";
PropManager mockProp = Mockito.mock(PropManager.class);
Mockito.when(mockProp.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("GW_GROUP.sys.template"))).thenReturn(template);
injectMockPropManager(mockProp);
EAIMessage eaiMessage = toEaiMessage(buildMsg("E001", "오류메시지", "",
new String[][]{{"E001", "오류A", "", ""}}));
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"GW_GROUP", "GW_ADAPTER", null, null, eaiMessage);
assertEquals(
"{\"aeh-sys-resultCode1\":\"E001\"," +
"\"aeh-sys-resultCode1\":\"E001\"," +
"\"aeh-sys-resultMsg1\":\"오류메시지\"}",
result);
}
}
// ================================================================
// generateNonStandardInboundErrorResponseMessage (인바운드 오류 응답)
// 템플릿 키: {adapterGroupId}.in.{httpCode}.template → {adapterGroupId}.in.template 순서로 조회
// ================================================================
@Nested
@DisplayName("generateNonStandardInboundErrorResponseMessage")
class GenerateInboundMethod {
private AdapterManager mockAdapterManager;
private AdapterPropManager mockAdapterPropManager;
private PropManager mockPropManager;
private AdapterGroupVO mockAdapterGroupVO;
private AdapterVO mockAdapterVO;
@BeforeEach
void setUpInbound() throws Exception {
mockAdapterManager = Mockito.mock(AdapterManager.class);
mockAdapterPropManager = Mockito.mock(AdapterPropManager.class);
mockPropManager = Mockito.mock(PropManager.class);
mockAdapterGroupVO = Mockito.mock(AdapterGroupVO.class);
mockAdapterVO = Mockito.mock(AdapterVO.class);
Mockito.when(mockAdapterGroupVO.getMessageType()).thenReturn("JSON");
Mockito.when(mockAdapterGroupVO.getMessageEncode()).thenReturn("UTF-8");
Mockito.when(mockAdapterVO.getPropGroupName()).thenReturn("TEST_PROP");
Mockito.when(mockAdapterVO.getAdapterGroupVO()).thenReturn(mockAdapterGroupVO);
Mockito.when(mockAdapterManager.getAdapterGroupVO("TEST_GROUP")).thenReturn(mockAdapterGroupVO);
Mockito.when(mockAdapterManager.getAdapterVO("TEST_GROUP", "TEST_ADAPTER")).thenReturn(mockAdapterVO);
Properties httpProp = new Properties();
httpProp.setProperty("ERROR_RESPONSE_FORMAT", "");
Mockito.when(mockAdapterPropManager.getProperties("TEST_PROP")).thenReturn(httpProp);
Mockito.when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null);
// 3-인자 getProperty(group, key, defaultValue) → defaultValue 반환
// (MessageUtil.makeJsonErrorMessage 에서 ERROR_MESSAGE_DEFAULT_FORMAT 폴백 시 사용)
Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
.thenAnswer(invocation -> invocation.getArgument(2));
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
Mockito.when(mockCtx.getBean(AdapterManager.class)).thenReturn(mockAdapterManager);
Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockAdapterPropManager);
Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
ctxField.setAccessible(true);
ctxField.set(null, mockCtx);
}
@Test
@DisplayName("HttpStatusException → httpCode 전용 템플릿 우선 사용")
void HttpStatusException_전용_템플릿() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.400.template")))
.thenReturn("{\"status\":\"400\",\"msg\":\"Bad Request\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new HttpStatusException("Bad Request", 400));
assertEquals("{\"status\":\"400\",\"msg\":\"Bad Request\"}", result);
}
@Test
@DisplayName("HttpStatusException → 전용 템플릿 없으면 공통 in.template 사용")
void HttpStatusException_공통_템플릿_폴백() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.404.template"))).thenReturn(null);
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.template")))
.thenReturn("{\"error\":\"not found\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new HttpStatusException("Not Found", 404));
assertEquals("{\"error\":\"not found\"}", result);
}
@Test
@DisplayName("JwtAuthException → 401 전용 템플릿 사용")
void JwtAuthException_401_전용_템플릿() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.401.template")))
.thenReturn("{\"error\":\"unauthorized\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new JwtAuthException("AUTH_ERR", "토큰 인증 실패"));
assertEquals("{\"error\":\"unauthorized\"}", result);
}
@Test
@DisplayName("JwtAuthException → 401 전용 없으면 공통 in.template 사용")
void JwtAuthException_공통_템플릿_폴백() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.401.template"))).thenReturn(null);
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.template")))
.thenReturn("{\"error\":\"auth failed\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new JwtAuthException("AUTH_ERR", "토큰 만료"));
assertEquals("{\"error\":\"auth failed\"}", result);
}
@Test
@DisplayName("기타 예외 → 500 전용 템플릿 사용")
void 기타예외_500_전용_템플릿() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.500.template")))
.thenReturn("{\"error\":\"internal server error\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new RuntimeException("처리 중 오류"));
assertEquals("{\"error\":\"internal server error\"}", result);
}
@Test
@DisplayName("기타 예외 → 500 전용 없으면 공통 in.template 사용")
void 기타예외_공통_템플릿_폴백() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.500.template"))).thenReturn(null);
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.template")))
.thenReturn("{\"error\":\"server error\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new RuntimeException("처리 실패"));
assertEquals("{\"error\":\"server error\"}", result);
}
@Test
@DisplayName("callProp 값이 inbound 템플릿에 치환")
void callProp_inbound_템플릿_치환() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.500.template")))
.thenReturn("{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"ifId\":\"${callprop.IF_ID}\"}");
Properties callProp = props("ADAPTER_NAME", "MY_ADAPTER", "IF_ID", "SVC001");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", callProp, null, null,
new RuntimeException("오류"));
assertEquals("{\"adapter\":\"MY_ADAPTER\",\"ifId\":\"SVC001\"}", result);
}
@Test
@DisplayName("inbound 템플릿은 msg=null 이므로 StandardMessage 경로는 빈 문자열")
void inbound_템플릿_stdmsg_경로_빈문자열() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.500.template")))
.thenReturn("{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd:NONE}\",\"fixed\":\"value\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new RuntimeException("오류"));
// render(template, null, callProp) → StandardMessage 경로는 기본값으로 처리
assertEquals("{\"code\":\"NONE\",\"fixed\":\"value\"}", result);
}
@Test
@DisplayName("inbound 템플릿에서 ${exception.xxx} 로 예외 필드 참조")
void inbound_템플릿_exception_치환() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.400.template")))
.thenReturn("{\"status\":\"${exception.status}\",\"msg\":\"${exception.message}\",\"code\":\"${exception.code}\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new HttpStatusException("Bad Request", "ERR_400", 400));
assertEquals("{\"status\":\"400\",\"msg\":\"Bad Request\",\"code\":\"ERR_400\"}", result);
}
@Test
@DisplayName("JwtAuthException 템플릿에서 ${exception.code} 참조")
void inbound_jwt_exception_code_치환() throws Exception {
Mockito.when(mockPropManager.getProperty(
eq(TemplateAdapterErrorMsgHandler.PROP_GROUP),
eq("TEST_GROUP.in.401.template")))
.thenReturn("{\"code\":\"${exception.code}\",\"msg\":\"${exception.message}\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다"));
assertEquals("{\"code\":\"JWT_EXPIRED\",\"msg\":\"토큰이 만료되었습니다\"}", result);
}
@Test
@DisplayName("템플릿 조회 순서: 전용(httpCode) → 공통(in) 순서로 조회")
void 템플릿_조회_순서_검증() throws Exception {
// 두 템플릿 모두 null → MessageUtil 폴백 (조회 순서만 검증)
handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new HttpStatusException("Service Unavailable", 503));
InOrder inOrder = Mockito.inOrder(mockPropManager);
inOrder.verify(mockPropManager).getProperty(
TemplateAdapterErrorMsgHandler.PROP_GROUP, "TEST_GROUP.in.503.template");
inOrder.verify(mockPropManager).getProperty(
TemplateAdapterErrorMsgHandler.PROP_GROUP, "TEST_GROUP.in.template");
}
@Test
@DisplayName("두 템플릿 모두 없으면 MessageUtil 폴백 결과가 non-null")
void 템플릿_없으면_MessageUtil_폴백() throws Exception {
// 모든 getProperty → null (기본 설정)
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"TEST_GROUP", "TEST_ADAPTER", null, null, null,
new RuntimeException("오류"));
// MessageUtil.makeErrorMessageByMessageType 가 호출되어 non-null 반환
assertNotNull(result);
}
}
}