diff --git a/src/main/resources/standard-layout-djb.csv b/src/main/resources/standard-layout-djb.csv index 3708b28..955efef 100644 --- a/src/main/resources/standard-layout-djb.csv +++ b/src/main/resources/standard-layout-djb.csv @@ -168,7 +168,7 @@ err_chrg_cnpl_no,3,2,0,1,20,1,,, # ============================================================ # DATA - 데이터부 # ============================================================ -DATA,1,3,1,1,0,1,,, +DATA,1,4,1,1,0,1,,, data_dvcd,2,2,1,1,2,1,,,IO data_scop_len,2,2,1,1,8,2,,, BIZ_DATA,2,9,1,1,0,1,,, diff --git a/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java b/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java index 63857c3..29d2c61 100644 --- a/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java +++ b/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyString; * 5. 비표준 수신 → 정상 응답 표준 생성 (coordinateAfterRecvNonStdSyncResponse → JSON) * 6. 오류 응답 표준 생성 (coordinateSetStandardMessageError → JSON) * 7. JSON 왕복(parse → serialize) 일관성 + * 8. 상대 헤더가 표준을 어긴 응답 파싱 (2026-09 사고 회귀 방지) */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class StandardMessageJsonFlowTest { @@ -80,11 +81,11 @@ class StandardMessageJsonFlowTest { + "\"frst_mesg_dman_dt\":\"20260420\"," + "\"frst_mesg_dman_time\":\"120000000\"" + "}," - + "\"DATA\":{" + + "\"DATA\":[{" + "\"data_dvcd\":\"IO\"," + "\"data_scop_len\":\"37\"," + "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"amt\":\"100000\"}" - + "}" + + "}]" + "}"; /** @@ -121,10 +122,58 @@ class StandardMessageJsonFlowTest { + "\"msg_list_rowcnt\":\"1\"" + "}" + "}," - + "\"DATA\":{" + + "\"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" + + "}" + "}" + "}"; @@ -208,7 +257,11 @@ class StandardMessageJsonFlowTest { void 요청JSON_파싱_DATA_BIZ_DATA_JSON_오브젝트_저장() throws Exception { StandardMessage msg = parseReq(); - String bizData = msg.findItemValue("DATA.BIZ_DATA"); + // 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 오브젝트로 파싱 가능한지 검증 @@ -344,9 +397,11 @@ class StandardMessageJsonFlowTest { String json = msg.getDataString(MessageType.JSON, "utf-8"); JsonNode root = jacksonMapper.readTree(json); + // DATA는 GRID(반복부)이므로 직렬화하면 JSON 배열이다. 표준 레이아웃상 1건 고정. // BIZ_DATA는 JSON 문자열 또는 JSON 오브젝트로 들어갈 수 있음 - assertNotNull(root.path("DATA").get("BIZ_DATA"), - "직렬화 JSON에 DATA.BIZ_DATA 없음"); + assertTrue(root.path("DATA").isArray(), "직렬화 JSON의 DATA가 배열이 아님"); + assertNotNull(root.path("DATA").path(0).get("BIZ_DATA"), + "직렬화 JSON에 DATA[0].BIZ_DATA 없음"); } // ================================================================ @@ -490,6 +545,40 @@ class StandardMessageJsonFlowTest { "응답 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 // ================================================================ @@ -505,4 +594,10 @@ class StandardMessageJsonFlowTest { 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; + } }