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() { 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; } }