Files
eapim-online/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java
T
curry772 ec6b51f2a8 정상 응답 MSG 코드/메시지를 레이아웃 기본값 대신 명시적으로 설정
outp_msg_cd(NCMM00001) / outp_msg_ctnt(정상처리되었습니다.) 가 표준전문 레이아웃의
기본값으로 붙어 있어서, "아무도 채우지 않은 상태" 와 "정상이라고 판정한 상태" 가
값만으로 구분되지 않았다. 2026-09 사고에서 MSG 부 파싱이 통째로 스킵됐는데도
오류가 "정상처리되었습니다." 로 보고된 것이 이 때문이다.

레이아웃 기본값을 없애고, StandardMessageCoordinatorDJB.coordinateBeforeResponse 에서
정상 응답일 때만 채운다. 이 메서드는 모든 동기 응답이 거치는 마지막 관문이라
(RequestProcessor 에서 호출) 경로를 빠짐없이 덮는다.

두 조건을 함께 본다.
 - 정상 응답(procs_rslt_dvcd=S) 일 때만. 오류 응답에 정상 메시지를 채우면 사고 재현이다.
 - 값이 비어 있을 때만(setIfBlank). 대외계가 표준응답으로 보낸 MSG 가 우선이며
   덮어쓰면 JsonReader 의 MSG 부 보존 수정이 무의미해진다.

테스트: 기존 3건이 coordinateAfterRecvNonStdSyncResponse 직후에 단정하고 있었으나
값은 이제 마지막 관문에서 채워지므로, 실제 흐름대로 coordinateBeforeResponse 까지
거치도록 고치고 단정도 "공백 아님" 에서 상수 일치로 강화했다.
설계 조건을 지키는 회귀 테스트 2건을 추가했다.
 - 오류응답에는_정상메시지가_채워지지_않는다
 - 대외계가_보낸_MSG_값은_덮어쓰지_않는다

주의: 이 CSV 는 layout.file.type=CSV 인 테스트 설정에서만 쓰인다. 운영은 DB 레이아웃을
읽으므로 DB 의 두 항목 기본값도 함께 비워야 실제 효과가 있다. 비우기 전까지는
setIfBlank 가 아무것도 하지 않아 기존과 동일하게 동작한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 10:20:13 +09:00

608 lines
26 KiB
Java

package com.eactive.eai.custom.message;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Field;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.context.ApplicationContext;
import com.eactive.eai.common.monitor.EAIServiceMonitor;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.message.StandardMessage;
import com.eactive.eai.message.manager.StandardMessageManager;
import com.eactive.eai.message.parser.JsonReader;
import com.eactive.eai.message.service.InterfaceMapper;
import com.eactive.eai.common.message.MessageType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.mockito.ArgumentMatchers.anyString;
/**
* DJBank 표준전문 JSON 처리 흐름 통합 테스트
*
* 검증 시나리오:
* 1. JSON → StandardMessage 파싱 (표준 요청 수신)
* 2. JSON → StandardMessage 파싱 (표준 응답 수신 — MSG 포함)
* 3. 표준 메시지 요청 수신 처리 (coordinateAfterCreateMessageByRule)
* 4. 표준 응답 생성 (coordinateBeforeResponse → JSON 직렬화)
* 5. 비표준 수신 → 정상 응답 표준 생성 (coordinateAfterRecvNonStdSyncResponse → JSON)
* 6. 오류 응답 표준 생성 (coordinateSetStandardMessageError → JSON)
* 7. JSON 왕복(parse → serialize) 일관성
* 8. 상대 헤더가 표준을 어긴 응답 파싱 (2026-09 사고 회귀 방지)
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StandardMessageJsonFlowTest {
// ----------------------------------------------------------------
// 테스트용 상수 (GUID 40자 고정)
// ----------------------------------------------------------------
private static final String TEST_GUID =
"20260420" + "120000" + "000" + "EAI" + "01" + "123456789012" + "000001";
// 8+6+3+3+2+12+6 = 40자
private static final String TEST_IF_ID = "IF_DJBTEST_001";
private static final String TEST_TX_ID = "TXTEST000001";
private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
/**
* 표준 요청 JSON (dman_rspn_dvcd=S → MSG 블록 미포함)
* BIZ_DATA는 JSON 오브젝트 — 파싱 후 JSON 문자열로 저장됨
*/
private static final String REQ_JSON =
"{"
+ "\"HEAD\":{"
+ "\"nxgn_stnd_idfr\":\"JERA\","
+ "\"guid\":\"" + TEST_GUID + "\","
+ "\"guid_prgs_no\":\"1\","
+ "\"stnd_mesg_ver\":\"R10\","
+ "\"ortr_guid\":\"" + TEST_GUID + "\","
+ "\"dman_rspn_dvcd\":\"S\","
+ "\"if_id\":\"" + TEST_IF_ID + "\","
+ "\"procs_rslt_dvcd\":\"S\","
+ "\"tx_id\":\"" + TEST_TX_ID + "\","
+ "\"chnl_tycd\":\"EAI\","
+ "\"hmab_dvcd\":\"1\","
+ "\"trnm_sys_dvcd\":\"OPA\","
+ "\"frst_trnm_sys_dvcd\":\"OPA\","
+ "\"sys_env_dvcd\":\"D\","
+ "\"frst_mesg_dman_dt\":\"20260420\","
+ "\"frst_mesg_dman_time\":\"120000000\""
+ "},"
+ "\"DATA\":[{"
+ "\"data_dvcd\":\"IO\","
+ "\"data_scop_len\":\"37\","
+ "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"amt\":\"100000\"}"
+ "}]"
+ "}";
/**
* 표준 응답 JSON (dman_rspn_dvcd=R → MSG 블록 포함, 정상)
*/
private static final String RSP_JSON_OK =
"{"
+ "\"HEAD\":{"
+ "\"nxgn_stnd_idfr\":\"JERA\","
+ "\"guid\":\"" + TEST_GUID + "\","
+ "\"guid_prgs_no\":\"2\","
+ "\"stnd_mesg_ver\":\"R10\","
+ "\"ortr_guid\":\"" + TEST_GUID + "\","
+ "\"dman_rspn_dvcd\":\"R\","
+ "\"if_id\":\"" + TEST_IF_ID + "\","
+ "\"procs_rslt_dvcd\":\"S\","
+ "\"tx_id\":\"" + TEST_TX_ID + "\","
+ "\"chnl_tycd\":\"EAI\","
+ "\"hmab_dvcd\":\"1\","
+ "\"trnm_sys_dvcd\":\"EAI\","
+ "\"sys_env_dvcd\":\"D\","
+ "\"frst_mesg_dman_dt\":\"20260420\","
+ "\"frst_mesg_dman_time\":\"120000000\","
+ "\"mesg_rspn_dt\":\"20260420\","
+ "\"mesg_rspn_time\":\"120100000\""
+ "},"
+ "\"MSG\":{"
+ "\"msg_dvcd\":\"NM\","
+ "\"msg_scop_len\":\"0\","
+ "\"MAIN_MSG\":{"
+ "\"outp_atrb_cd\":\"0\","
+ "\"outp_msg_cd\":\"000000000000\","
+ "\"outp_msg_ctnt\":\"정상처리\","
+ "\"msg_list_rowcnt\":\"1\""
+ "}"
+ "},"
+ "\"DATA\":[{"
+ "\"data_dvcd\":\"IO\","
+ "\"data_scop_len\":\"37\","
+ "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"bal\":\"900000\"}"
+ "}]"
+ "}";
/**
* 상대 헤더가 표준을 어긴 오류응답 JSON (2026-09 실제 사고 전문 형태)
*
* 표준은 응답 시 dman_rspn_dvcd=R 이어야 하는데, 상대가 요청값 S 를 그대로 에코백하면서
* procs_rslt_dvcd 만 F(오류)로 보냈다. MSG 블록의 레이아웃 조건이 !S 라 예전에는
* MSG 이하가 통째로 버려졌고, MAIN_MSG 가 레이아웃 기본값으로 남아 오류가
* "정상처리되었습니다." 로 보고되고 거래로그에서도 MSG 부가 누락됐다.
*
* 같은 전문에 들어있던 다른 비표준 요소도 함께 담아 둔다.
* - msg_list_rowcnt 가 따옴표 없는 선행 0 숫자(00001)
* - outp_msg_desc 안에 이스케이프되지 않은 개행/탭
*/
private static final String RSP_JSON_BAD_HEADER =
"{"
+ "\"HEAD\":{"
+ "\"nxgn_stnd_idfr\":\"JERA\","
+ "\"guid\":\"" + TEST_GUID + "\","
+ "\"guid_prgs_no\":\"2\","
+ "\"stnd_mesg_ver\":\"R10\","
+ "\"ortr_guid\":\"" + TEST_GUID + "\","
+ "\"dman_rspn_dvcd\":\"S\"," // 표준 위반: 응답인데 R 이 아님
+ "\"if_id\":\"" + TEST_IF_ID + "\","
+ "\"procs_rslt_dvcd\":\"F\","
+ "\"tx_id\":\"" + TEST_TX_ID + "\","
+ "\"chnl_tycd\":\"EAI\","
+ "\"hmab_dvcd\":\"1\","
+ "\"trnm_sys_dvcd\":\"EAI\","
+ "\"sys_env_dvcd\":\"D\","
+ "\"frst_mesg_dman_dt\":\"20260420\","
+ "\"frst_mesg_dman_time\":\"120000000\","
+ "\"mesg_rspn_dt\":\"20260420\","
+ "\"mesg_rspn_time\":\"120100000\","
+ "\"rprs_msg_cd\":\"900400\""
+ "},"
+ "\"MSG\":{"
+ "\"msg_dvcd\":\"EM\","
+ "\"msg_scop_len\":1437,"
+ "\"MAIN_MSG\":{"
+ "\"outp_atrb_cd\":\"0\","
+ "\"outp_msg_cd\":\"900400\","
+ "\"outp_msg_ctnt\":\"\","
+ "\"outp_msg_desc\":\"[ErrorCode:S9OWP0011].WTC.Adapter.\n\t호출 오류\","
+ "\"mngm_msg_cd\":\"\","
+ "\"msg_list_rowcnt\":00001"
+ "}"
+ "}"
+ "}";
private StandardMessageManager manager;
private InterfaceMapper mapper;
private JsonReader jsonReader;
private StandardMessageCoordinatorDJB coordinator;
private ObjectMapper jacksonMapper;
@BeforeAll
void setUpAll() throws Exception {
PropManager mockPropManager = Mockito.mock(PropManager.class);
Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
.thenAnswer(new Answer<String>() {
public String answer(InvocationOnMock inv) { return (String) inv.getArguments()[2]; }
});
EAIServerManager mockServer = Mockito.mock(EAIServerManager.class);
Mockito.when(mockServer.isPEAIServer()).thenReturn(false);
Mockito.when(mockServer.isSEAIServer()).thenReturn(false);
Mockito.when(mockServer.isMCI()).thenReturn(false);
Mockito.when(mockServer.getLocalServerName()).thenReturn("TEST_SERVER");
Mockito.when(mockServer.getGroupInstId()).thenReturn("TEST_INST");
EAIServiceMonitor mockMonitor = Mockito.mock(EAIServiceMonitor.class);
Mockito.when(mockMonitor.isTimeOutCodes(anyString())).thenReturn(false);
Mockito.when(mockMonitor.isBizErrorCodes(anyString())).thenReturn(false);
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
Mockito.when(mockCtx.getBean(EAIServerManager.class)).thenReturn(mockServer);
Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
Mockito.when(mockCtx.getBean(EAIServiceMonitor.class)).thenReturn(mockMonitor);
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
ctxField.setAccessible(true);
ctxField.set(null, mockCtx);
System.setProperty(StandardMessageManager.STANDARD_MESSAGE_CONFIG,
"src/test/resources/standard-message-config-test.properties");
manager = StandardMessageManager.getInstance();
manager.init();
}
@BeforeEach
void setUp() {
mapper = manager.getMapper();
jsonReader = (JsonReader) manager.getReader("JSON");
coordinator = new StandardMessageCoordinatorDJB();
jacksonMapper = new ObjectMapper();
}
// ================================================================
// 1. 표준 요청 JSON 파싱
// ================================================================
@Test
void 요청JSON_파싱_HEAD_guid_40자_정확() throws Exception {
StandardMessage msg = parseReq();
String guid = mapper.getGuid(msg);
assertEquals(TEST_GUID, guid, "파싱된 guid가 입력 JSON과 다름");
assertEquals(GUIDGeneratorDJB.GUID_LENGTH, guid.length(), "GUID 길이가 40자가 아님");
}
@Test
void 요청JSON_파싱_HEAD_if_id_매핑() throws Exception {
StandardMessage msg = parseReq();
assertEquals(TEST_IF_ID, msg.findItemValue("HEAD.if_id").trim());
}
@Test
void 요청JSON_파싱_HEAD_dman_rspn_dvcd_S() throws Exception {
StandardMessage msg = parseReq();
assertEquals("S", msg.findItemValue("HEAD.dman_rspn_dvcd"),
"요청 방향은 dman_rspn_dvcd = S 여야 함");
}
@Test
void 요청JSON_파싱_DATA_BIZ_DATA_JSON_오브젝트_저장() throws Exception {
StandardMessage msg = parseReq();
// DATA는 GRID(반복부)라 값은 행(row 0)에 들어간다. 인덱스 없는 "DATA.BIZ_DATA"는
// 직렬화에 쓰이지 않는 템플릿 항목을 가리키므로 빈 값이 나온다.
// 운영 경로도 인덱스 형태를 쓴다 — standard-message-mapping-config-djb.properties:27
// BIZ_DATA=DATA[0].BIZ_DATA
String bizData = msg.findItemValue("DATA[0].BIZ_DATA");
assertNotNull(bizData, "BIZ_DATA가 null");
assertFalse(bizData.trim().isEmpty(), "BIZ_DATA가 공백");
// JSON 오브젝트로 파싱 가능한지 검증
JsonNode node = jacksonMapper.readTree(bizData);
assertEquals("123456789", node.path("acct_no").asText(), "BIZ_DATA.acct_no 불일치");
}
@Test
void 요청JSON_파싱_MSG_블록_사이즈_0_숨김() throws Exception {
StandardMessage msg = parseReq();
// dman_rspn_dvcd=S → MSG 블록은 조건 미충족 → size=0(숨김)
// JSON 직렬화 결과에 MSG 키가 없어야 함
String json = msg.toJson();
JsonNode root = jacksonMapper.readTree(json);
assertFalse(root.has("MSG"), "요청 전문 JSON에 MSG 블록이 포함되면 안 됨");
}
// ================================================================
// 2. 표준 응답 JSON 파싱 (MSG 포함)
// ================================================================
@Test
void 응답JSON_파싱_MSG_msg_dvcd_NM() throws Exception {
StandardMessage msg = parseRsp();
assertEquals("NM", msg.findItemValue("MSG.msg_dvcd"),
"정상 응답의 msg_dvcd는 NM이어야 함");
}
@Test
void 응답JSON_파싱_MSG_MAIN_MSG_outp_msg_ctnt_정상처리() throws Exception {
StandardMessage msg = parseRsp();
assertEquals("정상처리", msg.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"),
"정상 응답의 outp_msg_ctnt는 '정상처리'여야 함");
}
@Test
void 응답JSON_파싱_HEAD_procs_rslt_dvcd_S() throws Exception {
StandardMessage msg = parseRsp();
assertEquals("S", msg.findItemValue("HEAD.procs_rslt_dvcd"),
"정상 응답의 procs_rslt_dvcd는 S여야 함");
}
// ================================================================
// 3. 표준 메시지 수신 처리 — coordinateAfterCreateMessageByRule
// ================================================================
@Test
void 표준수신_coordinateAfterCreateMessageByRule_새GUID_40자_재설정() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
String newGuid = mapper.getGuid(msg);
assertNotNull(newGuid, "coordinateAfterCreateMessageByRule 후 GUID null");
assertEquals(GUIDGeneratorDJB.GUID_LENGTH, newGuid.length(), "재설정된 GUID가 40자가 아님");
// Coordinator는 새 GUID를 생성하므로 원본과 달라야 함
assertNotEquals(TEST_GUID, newGuid, "Coordinator가 기존 GUID를 그대로 사용함");
}
@Test
void 표준수신_coordinateAfterCreateMessageByRule_frst_mesg_dman_dt_오늘날짜() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
String today = LocalDate.now().format(FMT_DATE);
assertEquals(today, msg.findItemValue("HEAD.frst_mesg_dman_dt"),
"frst_mesg_dman_dt는 오늘 날짜여야 함");
}
@Test
void 표준수신_coordinateAfterCreateMessageByRule_trnm_sys_dvcd_EAI() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateAfterCreateMessageByRule(msg, null, null);
assertEquals("EAI", msg.findItemValue("HEAD.trnm_sys_dvcd"),
"coordinateAfterCreateMessageByRule 후 trnm_sys_dvcd는 EAI여야 함");
}
// ================================================================
// 4. 표준 응답 생성 — coordinateBeforeResponse + JSON 직렬화
// ================================================================
@Test
void 표준응답생성_coordinateBeforeResponse_mesg_rspn_dt_오늘날짜() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String today = LocalDate.now().format(FMT_DATE);
assertEquals(today, msg.findItemValue("HEAD.mesg_rspn_dt"),
"mesg_rspn_dt는 오늘 날짜여야 함");
}
@Test
void 표준응답생성_coordinateBeforeResponse_mesg_rspn_time_9자리() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String time = msg.findItemValue("HEAD.mesg_rspn_time");
assertNotNull(time, "mesg_rspn_time이 null");
assertEquals(9, time.length(), "mesg_rspn_time은 HHmmssSSS(9자)여야 함: " + time);
assertTrue(time.matches("\\d{9}"), "mesg_rspn_time 형식 오류");
}
@Test
void 표준응답생성_JSON_직렬화_HEAD_포함() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String json = msg.getDataString(MessageType.JSON, "utf-8");
assertNotNull(json, "직렬화 JSON이 null");
JsonNode root = jacksonMapper.readTree(json);
assertTrue(root.has("HEAD"), "직렬화 JSON에 HEAD 블록 없음");
}
@Test
void 표준응답생성_JSON_직렬화_guid_값_보존() throws Exception {
StandardMessage msg = parseReq();
// Coordinator 호출 없이 파싱된 상태 그대로 직렬화
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals(TEST_GUID, root.path("HEAD").path("guid").asText(),
"직렬화 JSON의 guid가 원본과 다름");
}
@Test
void 표준응답생성_JSON_직렬화_BIZ_DATA_보존() throws Exception {
StandardMessage msg = parseReq();
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
// DATA는 GRID(반복부)이므로 직렬화하면 JSON 배열이다. 표준 레이아웃상 1건 고정.
// BIZ_DATA는 JSON 문자열 또는 JSON 오브젝트로 들어갈 수 있음
assertTrue(root.path("DATA").isArray(), "직렬화 JSON의 DATA가 배열이 아님");
assertNotNull(root.path("DATA").path(0).get("BIZ_DATA"),
"직렬화 JSON에 DATA[0].BIZ_DATA 없음");
}
// ================================================================
// 5. 비표준 수신 → 표준 정상 응답 생성
// (외부 시스템에서 온 비표준 응답을 표준 성공 응답으로 변환)
// ================================================================
@Test
void 비표준수신_coordinateAfterRecvNonStdSyncResponse_procs_rslt_dvcd_S() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
assertEquals("S", msg.findItemValue("HEAD.procs_rslt_dvcd"),
"비표준 정상수신 후 procs_rslt_dvcd는 S여야 함");
}
@Test
void 비표준수신_coordinateAfterRecvNonStdSyncResponse_MSG_msg_dvcd_NM() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
assertEquals("NM", msg.findItemValue("MSG.msg_dvcd"),
"비표준 정상수신 후 msg_dvcd는 NM이어야 함");
}
@Test
void 비표준수신_정상응답_JSON_직렬화_NM_포함() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertTrue(root.has("MSG"), "정상응답 JSON에 MSG 블록 없음");
assertEquals("NM", root.path("MSG").path("msg_dvcd").asText(),
"정상응답 JSON의 msg_dvcd가 NM이 아님");
}
@Test
void 비표준수신_정상응답_JSON_직렬화_outp_msg_ctnt_설정됨() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
// outp_msg_cd/ctnt 는 레이아웃 기본값이 없으므로, 실제 흐름대로 응답 직전 조정까지
// 거쳐야 채워진다 (StandardMessageCoordinatorDJB.coordinateBeforeResponse)
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
String ctnt = root.path("MSG").path("MAIN_MSG").path("outp_msg_ctnt").asText();
assertEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CTNT, ctnt,
"정상응답 JSON의 outp_msg_ctnt가 설정되어야 함");
}
// ================================================================
// 6. 오류 응답 표준 생성 — coordinateSetStandardMessageError + JSON 직렬화
// ================================================================
@Test
void 오류응답_coordinateSetStandardMessageError_procs_rslt_dvcd_F() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "테스트 오류");
assertEquals("F", msg.findItemValue("HEAD.procs_rslt_dvcd"),
"오류 응답의 procs_rslt_dvcd는 F여야 함");
}
@Test
void 오류응답_JSON_직렬화_MSG_블록_포함() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "테스트 오류");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertTrue(root.has("MSG"), "오류응답 JSON에 MSG 블록이 없음");
}
@Test
void 오류응답_JSON_직렬화_msg_dvcd_EM() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "테스트 오류");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals("EM", root.path("MSG").path("msg_dvcd").asText(),
"오류응답 JSON의 msg_dvcd가 EM이 아님");
}
@Test
void 오류응답_JSON_직렬화_outp_atrb_cd_1_팝업() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "테스트 오류");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals("1",
root.path("MSG").path("MAIN_MSG").path("outp_atrb_cd").asText(),
"오류응답 JSON의 outp_atrb_cd가 1(팝업)이 아님");
}
@Test
void 오류응답_JSON_직렬화_procs_rslt_dvcd_F_HEAD에_포함() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateSetStandardMessageError(msg, mapper, "RECEAICMM999", "테스트 오류");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals("F", root.path("HEAD").path("procs_rslt_dvcd").asText(),
"오류응답 JSON의 HEAD.procs_rslt_dvcd가 F가 아님");
}
// ================================================================
// 7. JSON 왕복 일관성 (parse → serialize)
// ================================================================
@Test
void JSON_왕복_파싱후_직렬화_guid_일치() throws Exception {
StandardMessage msg = parseReq();
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals(TEST_GUID, root.path("HEAD").path("guid").asText(),
"JSON 왕복 후 guid 불일치");
}
@Test
void JSON_왕복_파싱후_직렬화_if_id_일치() throws Exception {
StandardMessage msg = parseReq();
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertEquals(TEST_IF_ID, root.path("HEAD").path("if_id").asText().trim(),
"JSON 왕복 후 if_id 불일치");
}
@Test
void JSON_왕복_응답JSON_파싱후_직렬화_MSG_보존() throws Exception {
StandardMessage msg = parseRsp();
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertTrue(root.has("MSG"), "응답 JSON 왕복 후 MSG 블록 소실");
assertEquals("NM", root.path("MSG").path("msg_dvcd").asText(),
"응답 JSON 왕복 후 msg_dvcd 불일치");
}
// ================================================================
// 8. 상대 헤더가 표준을 어긴 응답 파싱 (2026-09 사고 회귀 방지)
// ================================================================
@Test
void 상대헤더오류_dman_rspn_dvcd가_S여도_MSG부가_파싱됨() throws Exception {
StandardMessage msg = parseRspBadHeader();
assertEquals("F", msg.findItemValue("HEAD.procs_rslt_dvcd"),
"HEAD가 파싱되지 않음 — 전제 조건 실패");
assertEquals("EM", msg.findItemValue("MSG.msg_dvcd"),
"MSG.msg_dvcd 미반영 — MSG 블록이 버려졌다");
assertEquals("900400", msg.findItemValue("MSG.MAIN_MSG.outp_msg_cd"),
"outp_msg_cd가 레이아웃 기본값(NCMM00001) 그대로 — MSG부가 버려졌다");
assertNotEquals("정상처리되었습니다.", msg.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"),
"오류응답인데 레이아웃 기본값 '정상처리되었습니다.'가 그대로 남음");
assertNotNull(msg.findItemValue("MSG.MAIN_MSG.outp_msg_desc"),
"outp_msg_desc가 null — 개행/탭이 섞인 값이 파싱되지 않았다");
}
@Test
void 상대헤더오류_재직렬화시_MSG부_보존() throws Exception {
StandardMessage msg = parseRspBadHeader();
// 거래로그(EAILogDAO)는 수신 원문이 아니라 StandardMessage 재직렬화본을 저장한다.
// MSG가 hidden 이면 로그에서도 MSG부 이후가 통째로 누락된다.
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
assertTrue(root.has("MSG"), "재직렬화 결과에 MSG 블록 없음 — 거래로그에 MSG부 누락");
assertEquals("900400", root.path("MSG").path("MAIN_MSG").path("outp_msg_cd").asText(),
"재직렬화 결과의 outp_msg_cd 불일치");
}
// ================================================================
// helper
// ================================================================
private StandardMessage parseReq() throws Exception {
StandardMessage msg = manager.getStandardMessage();
jsonReader.parse(msg, REQ_JSON);
return msg;
}
private StandardMessage parseRsp() throws Exception {
StandardMessage msg = manager.getStandardMessage();
jsonReader.parse(msg, RSP_JSON_OK);
return msg;
}
private StandardMessage parseRspBadHeader() throws Exception {
StandardMessage msg = manager.getStandardMessage();
jsonReader.parse(msg, RSP_JSON_BAD_HEADER);
return msg;
}
}