표준 레이아웃 DATA 를 GRID 로 정정 + MSG부 파싱 회귀 테스트 추가
standard-layout-djb.csv 의 DATA 가 GROUP(3)으로 선언돼 있어, 매핑설정의
BIZ_DATA=DATA[0].BIZ_DATA 인덱스 접근이 실패했다. getBizDataBytes() 가 null 을
반환하고 makeupDataScopLen 이 이를 그대로 역참조해 NPE 가 나면서
coordinateBeforeRequest/Response 가 통째로 실패, 관련 테스트 9건이 깨져 있었다.
운영 레이아웃(DB)대로 GRID(4)로 정정한다. 이 CSV 는 layout.file.type=CSV 인
테스트 설정에서만 사용된다.
- DATA 픽스처를 실제 전문과 같은 배열 형태로 변경
- GRID 는 인덱스 없는 경로가 직렬화에 쓰이지 않는 템플릿을 가리키므로, 조회/단정
경로를 운영과 같은 DATA[0] 형태로 변경
- 상대 헤더가 표준을 어긴 오류응답(dman_rspn_dvcd=S) 회귀 테스트 2건 추가.
같은 전문에 있던 선행 0 숫자(00001)와 이스케이프되지 않은 개행/탭도 픽스처에 포함
알려진 별건: DATA 가 GRID 이므로 StandardMessageCoordinatorDJB.DATA_SCOP_LEN
("DATA.data_scop_len") 이 행이 아닌 템플릿에 기록되어 data_scop_len 이 재계산되지
않고 수신값이 그대로 나간다. JSON 경로에서는 영향이 없어 별도 건으로 둔다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user