diff --git a/elink-online-common b/elink-online-common index 024b24a..5dae063 160000 --- a/elink-online-common +++ b/elink-online-common @@ -1 +1 @@ -Subproject commit 024b24af49794c814e0882859f3cd119e07f3b9f +Subproject commit 5dae063fe5e3f7bb54fc0a434d9ab233f96342bc diff --git a/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorDJB.java b/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorDJB.java index b287b46..31c662a 100644 --- a/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorDJB.java +++ b/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorDJB.java @@ -205,6 +205,9 @@ public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordin InterfaceMapper mapper = manager.getMapper(); mapper.setResponseType(responseMessage, STDMessageKeys.RESPONSE_TYPE_CODE_N); + // 요청응답구분 = 응답(R) — dman_rspn_dvcd 기본값이 S(요청)이므로 반드시 설정 + mapper.setSendRecvDivision(responseMessage, STDMessageKeys.SEND_RECV_CD_RECV); + // MSG 영역 활성화 StandardItem msgItem = responseMessage.findItem("MSG"); if (msgItem != null) { diff --git a/src/test/java/com/eactive/eai/custom/message/StandardMessageFlatFlowTest.java b/src/test/java/com/eactive/eai/custom/message/StandardMessageFlatFlowTest.java new file mode 100644 index 0000000..03b361c --- /dev/null +++ b/src/test/java/com/eactive/eai/custom/message/StandardMessageFlatFlowTest.java @@ -0,0 +1,534 @@ +package com.eactive.eai.custom.message; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; + +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.StandardItem; +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.parser.StandardReader; +import com.eactive.eai.message.service.InterfaceMapper; + +import static org.mockito.ArgumentMatchers.anyString; + +/** + * DJBank 표준전문 FLAT 직렬화/파싱 흐름 통합 테스트 + * + * 검증 시나리오: + * 1. FlatReader 등록 확인 + * 2. 비ERP 요청 FLAT 왕복 — HEAD 보존, EZDATA/MSG 바이트 미포함 + * 3. ERP 요청 FLAT 왕복 — EZDATA 포함 보존 + * 4. 오류 응답 FLAT 왕복 — MSG EM 포함 보존 + * 5. 정상 응답 FLAT 왕복 — MSG NM 포함 보존 + * 6. 거래로그 시나리오 — FLAT 바이트 길이 검증 (조건부 블록 포함 여부) + * + * 핵심 버그 (EZDATA FLAT 오류): + * JSON 파싱 시 JSON에 없는 조건부 블록(EZDATA, MSG)은 isHidden=false 로 남는다. + * toByteArray() GROUP 케이스에서 size=0 이면서 refPath/refValue 가 있는 경우 + * isHidden 체크만으로는 불충분 → FLAT에 비활성 블록 바이트가 잘못 포함된다. + * + * 수정: toByteArray() / getBytesDataLength() GROUP 케이스를 + * toJson() 과 동일하게 size==0 이면 무조건 skip 으로 변경 + * (StandardItem.java 두 곳) + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class StandardMessageFlatFlowTest { + + 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_FLAT01"; + private static final String TEST_TX_ID = "TXTEST000003"; + private static final String FLAT_CHARSET = "euc-kr"; + + /** 비ERP 요청 JSON — dman_rspn_dvcd=S, corp_tlwn_virt_brcd 미설정 */ + private static final String NON_ERP_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\":\"00000037\"," + + "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"amt\":\"100000\"}" + + "}" + + "}"; + + /** ERP 요청 JSON — corp_tlwn_virt_brcd=ERP, EZDATA 포함 */ + private static final String ERP_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\"," + + "\"corp_tlwn_virt_brcd\":\"ERP\"," + + "\"trnm_sys_dvcd\":\"OPA\"," + + "\"frst_trnm_sys_dvcd\":\"OPA\"," + + "\"sys_env_dvcd\":\"D\"," + + "\"frst_mesg_dman_dt\":\"20260420\"," + + "\"frst_mesg_dman_time\":\"120000000\"" + + "}," + + "\"EZDATA\":{" + + "\"login_mhod_dvcd\":\"1\"," + + "\"elfn_bnkn_dvcd\":\"01\"," + + "\"usr_no\":\"USR0000000000001\"," + + "\"rgsr_id\":\"REG_ID_000000001\"," + + "\"secu_media_dvcd\":\"00\"," + + "\"secu_media_sta_dvcd\":\"00\"," + + "\"brll_srcd_yn\":\"N\"," + + "\"next_data_exis_yn\":\"N\"," + + "\"page_no\":\"00001\"," + + "\"page_size\":\"00020\"," + + "\"dup_tx_yn\":\"N\"" + + "}," + + "\"DATA\":{" + + "\"data_dvcd\":\"IO\"," + + "\"data_scop_len\":\"00000037\"," + + "\"BIZ_DATA\":{\"acct_no\":\"987654321\",\"amt\":\"500000\"}" + + "}" + + "}"; + + private StandardMessageManager manager; + private InterfaceMapper mapper; + private StandardReader flatReader; + private JsonReader jsonReader; + private StandardMessageCoordinatorDJB coordinator; + + @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(); + flatReader = manager.getReader("FLAT"); + jsonReader = (JsonReader) manager.getReader("JSON"); + coordinator = new StandardMessageCoordinatorDJB(); + } + + // ================================================================ + // 1. FlatReader 등록 확인 + // ================================================================ + + @Test + void FlatReader_등록_확인() { + assertNotNull(flatReader, "FlatReader가 등록되지 않음 (reader.FLAT 설정 확인)"); + } + + // ================================================================ + // 2. 비ERP 요청 FLAT 왕복 — HEAD 보존, EZDATA/MSG 비포함 + // + // [버그] 수정 전: EZDATA(1500B) + MSG(10B) 가 FLAT에 잘못 포함 + // → FlatReader 파싱 시 overflow 예외 발생 + // [수정] StandardItem.toByteArray() GROUP 케이스: size==0 이면 무조건 skip + // ================================================================ + + @Test + void 비ERP요청_FLAT_파싱_예외없음() throws Exception { + StandardMessage src = parseJson(NON_ERP_REQ_JSON); + String flatStr = src.toFixedString(false, FLAT_CHARSET); + + StandardMessage dst = manager.getStandardMessage(); + assertDoesNotThrow(() -> flatReader.parse(dst, flatStr), + "비ERP FLAT 파싱 중 예외 발생 — EZDATA 바이트 오버플로우 버그 확인 필요\n" + + "원인: StandardItem.toByteArray() GROUP size==0 skip 조건 미적용"); + } + + @Test + void 비ERP요청_FLAT_왕복_HEAD_guid_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + assertEquals(TEST_GUID, dst.findItemValue("HEAD.guid").trim(), + "FLAT 왕복 후 HEAD.guid 불일치"); + } + + @Test + void 비ERP요청_FLAT_왕복_HEAD_if_id_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + assertEquals(TEST_IF_ID, dst.findItemValue("HEAD.if_id").trim(), + "FLAT 왕복 후 HEAD.if_id 불일치"); + } + + @Test + void 비ERP요청_FLAT_왕복_HEAD_dman_rspn_dvcd_S() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + assertEquals("S", dst.findItemValue("HEAD.dman_rspn_dvcd").trim(), + "FLAT 왕복 후 HEAD.dman_rspn_dvcd 불일치"); + } + + @Test + void 비ERP요청_FLAT_왕복_HEAD_stnd_mesg_ver_R10() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + assertEquals("R10", dst.findItemValue("HEAD.stnd_mesg_ver").trim(), + "FLAT 왕복 후 HEAD.stnd_mesg_ver 불일치"); + } + + @Test + void 비ERP요청_FLAT_왕복_DATA_data_dvcd_IO() throws Exception { + // [버그] 수정 전: MSG 바이트가 잘못 포함 → DATA.data_dvcd 자리에 MSG 바이트 읽힘 ("NM") + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + assertEquals("IO", dst.findItemValue("DATA.data_dvcd").trim(), + "FLAT 왕복 후 DATA.data_dvcd 불일치 — MSG 바이트 오염 가능성"); + } + + @Test + void 비ERP요청_FLAT_왕복_EZDATA_비활성() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(NON_ERP_REQ_JSON)); + + StandardItem ezdataItem = dst.findItem("EZDATA"); + assertTrue(ezdataItem == null || ezdataItem.isHidden() || ezdataItem.getSize() == 0, + "비ERP FLAT 왕복 후 EZDATA 블록이 활성 상태이면 안 됨"); + } + + // ================================================================ + // 3. ERP 요청 FLAT 왕복 — EZDATA 포함 + // ================================================================ + + @Test + void ERP요청_FLAT_파싱_예외없음() throws Exception { + StandardMessage src = parseJson(ERP_REQ_JSON); + String flatStr = src.toFixedString(false, FLAT_CHARSET); + + StandardMessage dst = manager.getStandardMessage(); + assertDoesNotThrow(() -> flatReader.parse(dst, flatStr), + "ERP FLAT 파싱 중 예외 발생"); + } + + @Test + void ERP요청_FLAT_왕복_HEAD_guid_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals(TEST_GUID, dst.findItemValue("HEAD.guid").trim(), + "ERP FLAT 왕복 후 HEAD.guid 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_HEAD_corp_tlwn_virt_brcd_ERP() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("ERP", dst.findItemValue("HEAD.corp_tlwn_virt_brcd").trim(), + "ERP FLAT 왕복 후 HEAD.corp_tlwn_virt_brcd 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_활성() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + StandardItem ezdataItem = dst.findItem("EZDATA"); + assertNotNull(ezdataItem, "ERP FLAT 왕복 후 EZDATA 블록 없음"); + assertFalse(ezdataItem.isHidden(), "ERP FLAT 왕복 후 EZDATA 블록이 hidden 상태"); + assertTrue(ezdataItem.getSize() > 0, "ERP FLAT 왕복 후 EZDATA 블록 size가 0"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_login_mhod_dvcd_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("1", dst.findItemValue("EZDATA.login_mhod_dvcd").trim(), + "ERP FLAT 왕복 후 EZDATA.login_mhod_dvcd 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_elfn_bnkn_dvcd_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("01", dst.findItemValue("EZDATA.elfn_bnkn_dvcd").trim(), + "ERP FLAT 왕복 후 EZDATA.elfn_bnkn_dvcd 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_usr_no_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("USR0000000000001", dst.findItemValue("EZDATA.usr_no").trim(), + "ERP FLAT 왕복 후 EZDATA.usr_no 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_rgsr_id_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("REG_ID_000000001", dst.findItemValue("EZDATA.rgsr_id").trim(), + "ERP FLAT 왕복 후 EZDATA.rgsr_id 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_secu_media_dvcd_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("00", dst.findItemValue("EZDATA.secu_media_dvcd").trim(), + "ERP FLAT 왕복 후 EZDATA.secu_media_dvcd 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_dup_tx_yn_보존() throws Exception { + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("N", dst.findItemValue("EZDATA.dup_tx_yn").trim(), + "ERP FLAT 왕복 후 EZDATA.dup_tx_yn 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_NUMBER_page_no_보존() throws Exception { + // page_no 는 NUMBER 타입: "00001" → toTypeValue() 선행 0 제거 → "1" + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + String pageNo = dst.findItemValue("EZDATA.page_no").trim(); + assertFalse(pageNo.isEmpty(), "ERP FLAT 왕복 후 EZDATA.page_no가 비어 있음"); + assertEquals(1, Integer.parseInt(pageNo), + "ERP FLAT 왕복 후 EZDATA.page_no 값 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_EZDATA_NUMBER_page_size_보존() throws Exception { + // page_size 는 NUMBER 타입: "00020" → "20" + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + String pageSize = dst.findItemValue("EZDATA.page_size").trim(); + assertFalse(pageSize.isEmpty(), "ERP FLAT 왕복 후 EZDATA.page_size가 비어 있음"); + assertEquals(20, Integer.parseInt(pageSize), + "ERP FLAT 왕복 후 EZDATA.page_size 값 불일치"); + } + + @Test + void ERP요청_FLAT_왕복_DATA_data_dvcd_IO() throws Exception { + // [버그] 수정 전: MSG 바이트가 잘못 포함 → DATA.data_dvcd 자리에 MSG 바이트 읽힘 + StandardMessage dst = flatRoundTrip(parseJson(ERP_REQ_JSON)); + + assertEquals("IO", dst.findItemValue("DATA.data_dvcd").trim(), + "ERP FLAT 왕복 후 DATA.data_dvcd 불일치 — MSG 바이트 오염 가능성"); + } + + // ================================================================ + // 4. 오류 응답 FLAT 왕복 — MSG EM 포함 + // coordinateSetStandardMessageError() → MSG.size=1, MAIN_MSG.size=1 + // ================================================================ + + @Test + void 오류응답_FLAT_파싱_예외없음() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateSetStandardMessageError(src, mapper, "RECEAICMM999", "테스트 오류"); + String flatStr = src.toFixedString(false, FLAT_CHARSET); + + StandardMessage dst = manager.getStandardMessage(); + assertDoesNotThrow(() -> flatReader.parse(dst, flatStr), + "오류응답 FLAT 파싱 중 예외 발생"); + } + + @Test + void 오류응답_FLAT_왕복_HEAD_procs_rslt_dvcd_F() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateSetStandardMessageError(src, mapper, "RECEAICMM999", "테스트 오류"); + + assertEquals("F", flatRoundTrip(src).findItemValue("HEAD.procs_rslt_dvcd").trim(), + "오류응답 FLAT 왕복 후 HEAD.procs_rslt_dvcd가 F가 아님"); + } + + @Test + void 오류응답_FLAT_왕복_MSG_msg_dvcd_EM() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateSetStandardMessageError(src, mapper, "RECEAICMM999", "테스트 오류"); + + assertEquals("EM", flatRoundTrip(src).findItemValue("MSG.msg_dvcd").trim(), + "오류응답 FLAT 왕복 후 MSG.msg_dvcd가 EM이 아님"); + } + + @Test + void 오류응답_FLAT_왕복_MSG_outp_atrb_cd_1_팝업() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateSetStandardMessageError(src, mapper, "RECEAICMM999", "테스트 오류"); + + assertEquals("1", flatRoundTrip(src).findItemValue("MSG.MAIN_MSG.outp_atrb_cd").trim(), + "오류응답 FLAT 왕복 후 MSG.MAIN_MSG.outp_atrb_cd가 1(팝업)이 아님"); + } + + @Test + void 오류응답_FLAT_왕복_MSG_MAIN_MSG_활성() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateSetStandardMessageError(src, mapper, "RECEAICMM999", "테스트 오류"); + + StandardItem mainMsgItem = flatRoundTrip(src).findItem("MSG.MAIN_MSG"); + assertNotNull(mainMsgItem, "오류응답 FLAT 왕복 후 MSG.MAIN_MSG 블록 없음"); + assertTrue(mainMsgItem.getSize() > 0, + "오류응답 FLAT 왕복 후 MSG.MAIN_MSG size가 0"); + } + + // ================================================================ + // 5. 정상 응답 FLAT 왕복 — MSG NM 포함 + // coordinateAfterRecvNonStdSyncResponse() → MSG.size=1, MAIN_MSG.size=1 + // ================================================================ + + @Test + void 정상응답_FLAT_파싱_예외없음() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateAfterRecvNonStdSyncResponse(src); + String flatStr = src.toFixedString(false, FLAT_CHARSET); + + StandardMessage dst = manager.getStandardMessage(); + assertDoesNotThrow(() -> flatReader.parse(dst, flatStr), + "정상응답 FLAT 파싱 중 예외 발생"); + } + + @Test + void 정상응답_FLAT_왕복_MSG_msg_dvcd_NM() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateAfterRecvNonStdSyncResponse(src); + + assertEquals("NM", flatRoundTrip(src).findItemValue("MSG.msg_dvcd").trim(), + "정상응답 FLAT 왕복 후 MSG.msg_dvcd가 NM이 아님"); + } + + @Test + void 정상응답_FLAT_왕복_MSG_활성() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateAfterRecvNonStdSyncResponse(src); + StandardMessage dst = flatRoundTrip(src); + + StandardItem msgItem = dst.findItem("MSG"); + assertNotNull(msgItem, "정상응답 FLAT 왕복 후 MSG 블록 없음"); + assertFalse(msgItem.isHidden(), "정상응답 FLAT 왕복 후 MSG 블록이 hidden 상태"); + assertTrue(msgItem.getSize() > 0, "정상응답 FLAT 왕복 후 MSG 블록 size가 0"); + } + + @Test + void 정상응답_FLAT_왕복_MSG_MAIN_MSG_활성() throws Exception { + StandardMessage src = manager.getStandardMessage(); + coordinator.coordinateAfterRecvNonStdSyncResponse(src); + + StandardItem mainMsgItem = flatRoundTrip(src).findItem("MSG.MAIN_MSG"); + assertNotNull(mainMsgItem, "정상응답 FLAT 왕복 후 MSG.MAIN_MSG 블록 없음"); + assertTrue(mainMsgItem.getSize() > 0, + "정상응답 FLAT 왕복 후 MSG.MAIN_MSG size가 0"); + } + + // ================================================================ + // 6. 거래로그 시나리오 — FLAT 바이트 길이로 조건부 블록 포함 여부 검증 + // + // [버그] 수정 전: 비ERP 에도 EZDATA(1500B) 포함 → ERP 와 길이 동일 + // [수정] 비ERP FLAT < ERP FLAT (EZDATA 1500바이트 차이) + // ================================================================ + + @Test + void 거래로그_비ERP_FLAT_길이가_ERP_FLAT_보다_짧음() throws Exception { + byte[] nonErpBytes = parseJson(NON_ERP_REQ_JSON) + .toFixedString(false, FLAT_CHARSET).getBytes(FLAT_CHARSET); + byte[] erpBytes = parseJson(ERP_REQ_JSON) + .toFixedString(false, FLAT_CHARSET).getBytes(FLAT_CHARSET); + + // EZDATA = 1500바이트 — ERP FLAT 이 비ERP FLAT 보다 길어야 함 + // 버그 발생 시: 둘 다 EZDATA 포함 → 길이 동일 → 검증 실패 + assertTrue(erpBytes.length > nonErpBytes.length, + String.format("ERP FLAT 길이(%d) > 비ERP FLAT 길이(%d) 이어야 함 (EZDATA 1500B 차이)", + erpBytes.length, nonErpBytes.length)); + } + + @Test + void 거래로그_요청_FLAT_길이가_응답_FLAT_보다_짧음() throws Exception { + // 요청(dman_rspn_dvcd=S): MSG 비활성 / 응답: MSG 활성 → 응답이 더 긺 + StandardMessage rspMsg = manager.getStandardMessage(); + coordinator.coordinateAfterRecvNonStdSyncResponse(rspMsg); + + byte[] reqBytes = parseJson(NON_ERP_REQ_JSON) + .toFixedString(false, FLAT_CHARSET).getBytes(FLAT_CHARSET); + byte[] rspBytes = rspMsg.toFixedString(false, FLAT_CHARSET).getBytes(FLAT_CHARSET); + + // 버그 발생 시: 요청도 MSG 바이트 포함 → 길이 역전 가능 + assertTrue(rspBytes.length > reqBytes.length, + String.format("응답 FLAT 길이(%d) > 요청 FLAT 길이(%d) 이어야 함 (MSG 블록 포함 여부)", + rspBytes.length, reqBytes.length)); + } + + // ================================================================ + // helpers + // ================================================================ + + private StandardMessage parseJson(String json) throws Exception { + StandardMessage msg = manager.getStandardMessage(); + jsonReader.parse(msg, json); + return msg; + } + + /** + * FLAT 왕복: src → toFixedString(false, euc-kr) → FlatReader.parse() → dst + * STDMessageDAO.addRestoreSTDMessage / getRestoreSTDMessage 와 동일한 흐름 + */ + private StandardMessage flatRoundTrip(StandardMessage src) throws Exception { + String flatStr = src.toFixedString(false, FLAT_CHARSET); + StandardMessage dst = manager.getStandardMessage(); + flatReader.parse(dst, flatStr); + return dst; + } +}