8 Commits

Author SHA1 Message Date
curry772 858d72c4de XMLMessage BIGDECIMAL validation 추가 및 isValid() 공통화
- Converter.isValid(): JSONMessage의 private isValid()를 static 공통 메서드로 이동
  (지수형 정규화 포함, 빈 문자열은 검증 대상 아님)
- JSONMessage: Converter.isValid() 위임, private isValid() 제거
- XMLMessage: doSetValue()에서 TYPE_BIGDECIMAL 필드 validation 추가
  (try-catch 예외 삼킴 블록 앞에 위치시켜 예외가 전파되도록 함)
- BytesMessageValidationTest: VALID_RAW 길이 오류 수정(32→35바이트),
  TYPE_BYTEARRAY isolation 검증
- XMLMessageValidationTest: validation 정상/오류/경계 케이스 10개 추가
  (C-1 assertThrows 포함)
2026-05-20 13:15:32 +09:00
curry772 9860f31d83 Merge branch 'feature/json-scientific-normalize' 2026-05-20 10:10:45 +09:00
curry772 ac71355e05 지수형 부동소수점 validation 정규화 flag 추가 (transformer.scientific.normalize)
- SystemKeys: TRANSFROMER_SCIENTIFIC_NORMALIZE 키 및
isScientificNormalizeEnabled() 추가
- JSONMessage.isValid(): flag ON 시 toPlainString()으로 정규화 후 검증
- false positive 방지: 1.23e5(=123000)가 자릿수 제한을 우회하는 문제 차단
- false negative 방지: pointLength=0 필드에서 유효한 지수형 값이 거부되는 문제 해결
- JSONMessageValidationTest: 지수형 시나리오 4개 추가 (정규화 ON/OFF 각 케이스)
2026-05-20 10:09:08 +09:00
curry772 e0ea4a4f5d JSON 메시지 validation 기능 수정
- 오류발생위치 출력
- type 오류 validation test 추가
2026-05-18 14:00:28 +09:00
curry772 82c7c2bc8c JSON_VALIDATION_TEST_PLAN.md git 추적 제거 (작업내역_* 패턴으로 rename)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:34:33 +09:00
curry772 d469fac2d7 .gitignore에 작업내역 문서 패턴 추가
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:30:50 +09:00
curry772 2d3f659012 ConcatN 가변 파라미터 문자열 연결 함수 추가 및 단위테스트 작성
- ConcatN: numberOfParameters=-1로 파라미터 수 제한 없이 문자열 연결
- 기존 Concat(2개 고정)을 대체 가능하며 하위 호환성 유지
- ConcatNFunctionTest: JEP 직접 사용, 파라미터 수 변화 검증
- ConcatNParserTest: Parser 클래스 경유, 변수/상수 혼합 표현식 및 성능 검증

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 13:25:37 +09:00
curry772 17f6df5068 Merge branch 'feature/json-validation-test'
- JSONMessage BigDecimal 자릿수 검사를 setData() 시점으로 통일
- SystemKeys.isValidationEnabled() 하드코딩 버그 수정
- JSONMessageValidationTest 7개 테스트 추가
- JSON_VALIDATION_TEST_PLAN.md 작업 내역 문서화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 09:48:12 +09:00
15 changed files with 1201 additions and 206 deletions
+3
View File
@@ -231,3 +231,6 @@ gradle-app.setting
# End of https://www.gitignore.io/api/java,macos,gradle,intellij
/EAISIMWeb/
/.apt_generated_tests/
# 작업내역 문서
작업내역_*.md
-132
View File
@@ -1,132 +0,0 @@
# JSONMessage Validation 단위테스트 작업 내역
## 브랜치
`feature/json-validation-test`
## 커밋
`ae629d5` — JSONMessage validation 단위테스트 추가 및 SystemKeys 버그 수정
`(최신)` — doSetValue() 자릿수 검사 setData() 시점으로 이동 및 테스트 수정
---
## 1. 목표
`JSONMessage`의 두 가지 validation 오류 시나리오를 단위테스트로 검증한다.
| 시나리오 | 발생 시점 | 결과 |
|---|---|---|
| 숫자형 필드에 타입 불일치 문자열 입력 | `setData()` | ✅ 정상 동작 확인 |
| 숫자형 필드의 자릿수 초과 | `setData()` | ✅ `doSetValue()` 개선으로 setData() 시점으로 통일 |
---
## 2. 수정 파일
### 2-1. `SystemKeys.java` — 버그 수정
`isValidationEnabled()``validationEnabled` 필드를 무시하고 `false`를 하드코딩으로 반환하는 버그.
`reload()``transformer.validation` 시스템 프로퍼티를 읽어 필드에 저장하지만 반환값에 반영되지 않았다.
```java
// 수정 전
public static boolean isValidationEnabled() { return false; }
// 수정 후
public static boolean isValidationEnabled() { return validationEnabled; }
```
### 2-2. `init_json_validation.sql` — 신규
`JSONMessageValidationTest` 전용 테스트 데이터. 공유 init 파일(`init_tseaitr07/08.sql`)을 수정하지 않기 위해 별도 파일로 분리.
- `tseaitr07`: `EAI_UJSONTYPE_REQ1` 레이아웃 헤더 (타입: JSON)
- `tseaitr08`: 레이아웃 아이템 5건
| 필드명 | 타입코드 | length | decimal | 정수부 최대 |
|---|---|---|---|---|
| INT_VALUE | 2102 (INT) | 5 | 0 | — |
| LONG_VALUE | 2103 (LONG) | 10 | 0 | — |
| BIG_VALUE | 2118 (BIGDECIMAL) | 10 | 5 | 4자리 |
| BIG15_VALUE | 2118 (BIGDECIMAL) | 15 | 0 | 15자리 |
> 정수부 최대 = `length - decimal - 1`
### 2-3. `JSONMessageValidationTest.java` — 신규
`@DataJpaTest` + H2 인메모리 DB 기반 JUnit 5 테스트. 테스트 7개 전체 통과.
기존 방식(`AllTests.setParams()` + `TransformEngine.getInstance()`)은 Spring 컨텍스트 없이 실행 시 NPE가 발생했다. `@DataJpaTest` + `@ContextConfiguration(TransformJPATestConfiguration.class)` + `@Autowired TransformEngine`으로 교체해 해결.
### 2-4. `JSONMessage.java` — `doSetValue()` 수정
`doSetValue()``TYPE_BIGDECIMAL` case에 자릿수 검사 추가. 기존에는 자릿수 초과가 `getData()` 시점에 감지되어 타입 불일치와 발생 시점이 달랐다.
```java
// 수정 전
case Types.TYPE_BIGDECIMAL :
setBigDecimal( itemPath, new BigDecimal(value) );
break;
// 수정 후
case Types.TYPE_BIGDECIMAL :
if (SystemKeys.isValidationEnabled() && !isValid(value, item.getLength(), item.getDecimalPointLength())) {
throw new InvalidAccessException(
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d",
value, item.getLength(), item.getDecimalPointLength()),
itemPath);
}
setBigDecimal( itemPath, new BigDecimal(value) );
break;
```
```
@DataJpaTest
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
@Sql({
"/com/eactive/eai/transformer/init_tseaitr06.sql", // 레이아웃 타입 코드
"/com/eactive/eai/transformer/init_json_validation.sql" // EAI_UJSONTYPE_REQ1 레이아웃
})
```
---
## 3. 테스트 케이스
### 타입 불일치 — `setData()` 시점, validation 플래그 무관
| 테스트 메서드 | 입력 | 기대 결과 |
|---|---|---|
| `testIntFieldTypeError` | `"INT_VALUE":"abc"` | `MessageSetDataException` (cause: `NumberFormatException`) |
| `testLongFieldTypeError` | `"LONG_VALUE":"not_a_number"` | `MessageSetDataException` (cause: `NumberFormatException`) |
| `testBigDecimalFieldTypeError` | `"BIG_VALUE":"invalid_decimal"` | `MessageSetDataException` (cause: `NumberFormatException`) |
### 자릿수 초과 — `setData()` 시점, `transformer.validation=true` 필요
| 테스트 메서드 | 입력 | 기대 결과 |
|---|---|---|
| `testBigDecimalIntegerPartOverflow` | `"BIG_VALUE":12345.12345` (정수부 5자리) | `MessageSetDataException` ("Invalid decimal point format") |
| `testBigDecimalFractionalPartOverflow` | `"BIG_VALUE":1234.123456` (소수부 6자리) | `MessageSetDataException` ("Invalid decimal point format") |
| `testBigDecimalIntegerOnlyOverflow` | `"BIG15_VALUE":1234567890123456` (16자리) | `MessageSetDataException` ("Invalid decimal point format") |
| `testBigDecimalWithinBounds` | `"BIG_VALUE":1234.12345` (정수부 4자리, 소수부 5자리) | 정상 처리, 결과 not null |
---
## 4. 예외 전파 경로
```
[타입 불일치 — setData() 시점]
setData(json)
└─ doSetData()
└─ doSetValue()
└─ new Integer/Long/BigDecimal(value) ← NumberFormatException
└─ setValue() wraps → InvalidAccessException
└─ doSetData() wraps → MessageSetDataException
[자릿수 초과 — setData() 시점, validation=true]
setData(json)
└─ doSetData()
└─ doSetValue()
└─ isValid() false → throw InvalidAccessException
└─ setValue() wraps → InvalidAccessException
└─ doSetData() wraps → MessageSetDataException
```
@@ -7,6 +7,7 @@ public class SystemKeys {
public static final String TRANSFROMER_STRING_TRIMEND = "transformer.string.trimend";
public static final String TRANSFROMER_VALIDATION = "transformer.validation";
public static final String TRANSFROMER_DECIMALPOINT_ADD = "transformer.decimalpoint.add";
public static final String TRANSFROMER_SCIENTIFIC_NORMALIZE = "transformer.scientific.normalize";
public static final String TRANSFROMER_LAYOUT_CACHE_INIT_ONSTARTUP = "transformer.layout.cache.init.onstartup";
public static final String TRANSFROMER_LAYOUT_CACHE_SIZE = "transformer.layout.cache.size";
@@ -30,6 +31,7 @@ public class SystemKeys {
private static boolean stringTrimEnd = true;
private static boolean validationEnabled = true;
private static boolean addDecimalpoint = false;
private static boolean scientificNormalize = true;
private static boolean layoutCacheOnStartup = false;
@@ -64,7 +66,14 @@ public class SystemKeys {
} catch (Exception ex) {
addDecimalpoint = false;
}
sBoolean = System.getProperty(TRANSFROMER_SCIENTIFIC_NORMALIZE, "false");
try {
scientificNormalize = Boolean.valueOf(sBoolean);
} catch (Exception ex) {
scientificNormalize = false;
}
sBoolean = System.getProperty(TRANSFROMER_LAYOUT_CACHE_INIT_ONSTARTUP, "false");
try {
layoutCacheOnStartup = Boolean.valueOf(sBoolean);
@@ -93,6 +102,10 @@ public class SystemKeys {
return addDecimalpoint;
}
public static boolean isScientificNormalizeEnabled() {
return scientificNormalize;
}
public static boolean isLayoutCacheOnStartup() {
return layoutCacheOnStartup;
}
@@ -0,0 +1,38 @@
package com.eactive.eai.transformer.function.generic;
import java.util.Stack;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
public class ConcatN extends PostfixMathCommand {
public ConcatN() {
numberOfParameters = -1;
}
@SuppressWarnings("unchecked")
public void run(Stack inStack) throws ParseException {
checkStack(inStack);
int n = curNumberOfParameters;
if (n < 1) {
throw new ParseException("concatN() requires at least 1 parameter");
}
String[] params = new String[n];
for (int i = n - 1; i >= 0; i--) {
Object p = inStack.pop();
if (!(p instanceof String)) {
throw new ParseException("Invalid parameter type: parameter " + (i + 1) + " is not a string");
}
params[i] = (String) p;
}
StringBuilder sb = new StringBuilder();
for (String s : params) {
sb.append(s);
}
inStack.push(sb.toString());
}
}
@@ -211,6 +211,32 @@ public class Converter {
}
}
public static boolean isValid(String value, int length, int pointLength) {
String normalized = value.replaceAll(" ", "");
if (SystemKeys.isScientificNormalizeEnabled()) {
try {
normalized = new BigDecimal(normalized).toPlainString();
} catch (NumberFormatException e) {
return false;
}
}
String[] data = normalized.split("[.]");
if (pointLength > 0) {
if (data.length == 1) {
if (data[0].length() > length - pointLength - 1) return false;
} else if (data.length == 2) {
if (data[0].length() > length - pointLength - 1) return false;
if (data[1].length() > pointLength) return false;
} else {
return false;
}
} else {
if (data.length != 1) return false;
if (data[0].length() > length) return false;
}
return true;
}
public static String addPoint(String dpNum, int tLen , int dLen) {
if (dpNum == null || dpNum.trim().length() ==0 ){
return "0";
@@ -9,6 +9,7 @@ import java.util.Stack;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.transformer.SystemKeys;
import com.eactive.eai.transformer.layout.Converter;
import com.eactive.eai.transformer.layout.InvalidNodeException;
import com.eactive.eai.transformer.layout.Item;
import com.eactive.eai.transformer.layout.Types;
@@ -166,10 +167,12 @@ public class JSONMessage extends Message {
throw new InvalidAccessException("The number of repetitions is different from the number of repetitions of the actual data. , " + item.getName(), item.getItemPath());
}
} // validation
} catch(InvalidAccessException e) {
throw e;
} catch(Exception e) {
throw new InvalidAccessException("setValue error", getItemPath(), null, e);
}
}
}
protected void doSetValue(JsonNode node, HashMap validationTargets, String parentPath) {
Item item = null;
@@ -255,40 +258,49 @@ public class JSONMessage extends Message {
} else if(value == null || value.length() == 0) {
setObject( itemPath, value );
} else {
int itemType = item.getDataTypeId();
switch(itemType) {
case Types.TYPE_INT :
case Types.TYPE_SHORT :
setInt( itemPath, new Integer(value) );
break;
case Types.TYPE_LONG :
setLong( itemPath, new Long(value) );
break;
case Types.TYPE_BIGDECIMAL :
if (SystemKeys.isValidationEnabled() && !isValid(value, item.getLength(), item.getDecimalPointLength())) {
throw new InvalidAccessException(
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d",
value, item.getLength(), item.getDecimalPointLength()),
itemPath);
}
setBigDecimal( itemPath, new BigDecimal(value) );
break;
case Types.TYPE_DOUBLE :
setDouble( itemPath, new Double(value) );
break;
case Types.TYPE_FLOAT :
setFloat( itemPath, new Float(value) );
break;
case Types.TYPE_BOOLEAN :
setObject( itemPath, new Boolean(value) );
break;
case Types.TYPE_STRING :
setString(itemPath, value);
break;
default :
setString(itemPath, value);
break;
}
int itemType = item.getDataTypeId();
try {
switch(itemType) {
case Types.TYPE_INT :
case Types.TYPE_SHORT :
setInt( itemPath, new Integer(value) );
break;
case Types.TYPE_LONG :
setLong( itemPath, new Long(value) );
break;
case Types.TYPE_BIGDECIMAL :
if (SystemKeys.isValidationEnabled() && !Converter.isValid(value, item.getLength(), item.getDecimalPointLength())) {
throw new InvalidAccessException(
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d",
value, item.getLength(), item.getDecimalPointLength()),
itemPath);
}
setBigDecimal( itemPath, new BigDecimal(value) );
break;
case Types.TYPE_DOUBLE :
setDouble( itemPath, new Double(value) );
break;
case Types.TYPE_FLOAT :
setFloat( itemPath, new Float(value) );
break;
case Types.TYPE_BOOLEAN :
setObject( itemPath, new Boolean(value) );
break;
case Types.TYPE_STRING :
setString(itemPath, value);
break;
default :
setString(itemPath, value);
break;
}
} catch (InvalidAccessException e) {
throw e;
} catch (Exception e) {
throw new InvalidAccessException(
String.format("Type mismatch: value='%s', field='%s', dataType=%s",
value, itemPath, Types.getType(itemType)),
itemPath, null, e);
}
}
}
}
@@ -651,7 +663,7 @@ public class JSONMessage extends Message {
private String getFormat(String value, int length, int pointLength, boolean isValidation) throws Exception{
if(isValidation) {
if (!isValid(value,length,pointLength)){
if (!Converter.isValid(value,length,pointLength)){
throw new Exception(String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d"
, value,length,pointLength)
);
@@ -691,36 +703,6 @@ public class JSONMessage extends Message {
}
}
private boolean isValid(String value,int length,int pointLength){
String[] data = value.replaceAll(" ", "").split("[.]");
if (pointLength > 0 ){
if (data.length ==1){
if (data[0].length() > length - pointLength - 1 ){ //정수부 오류
return false;
}
}else if (data.length ==2){
if (data[0].length() > length - pointLength - 1 ){ //정수부 오류
return false;
}
if (data[1].length() > pointLength ){ //실수부 오류
return false;
}
}else{
//오류
return false;
}
}else{
if (data.length != 1){ //소수점이 들어와서 오류
return false;
}
if (data[0].length() > length){ //정수부 오류
return false;
}
}
return true;
}
// private Object toJsonObjectType(int itemType, String value) {
// Object jsonValueObject = null;
//
@@ -12,6 +12,7 @@ import org.dom4j.Namespace;
import org.dom4j.QName;
import com.eactive.eai.transformer.SystemKeys;
import com.eactive.eai.transformer.layout.Converter;
import com.eactive.eai.transformer.layout.InvalidNodeException;
import com.eactive.eai.transformer.layout.Item;
import com.eactive.eai.transformer.layout.Layout;
@@ -198,8 +199,17 @@ public class XMLMessage extends Message {
continue;
}
}
else if (item.isField()) {
else if (item.isField()) {
Object data = child.getData();
if (data instanceof String
&& item.getDataTypeId() == Types.TYPE_BIGDECIMAL
&& SystemKeys.isValidationEnabled()
&& !Converter.isValid((String) data, item.getLength(), item.getDecimalPointLength())) {
throw new InvalidAccessException(
String.format("Invalid decimal point format Error value=%s,length=%d,pointLength=%d",
data, item.getLength(), item.getDecimalPointLength()),
childPath);
}
try {
if (data instanceof String){
setString(childPath, (String) data);
@@ -0,0 +1,193 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.jdbc.Sql;
import com.eactive.eai.transformer.engine.TransformEngine;
import com.eactive.eai.transformer.function.jpa.TransformJPATestConfiguration;
import com.eactive.eai.transformer.message.Message;
import com.eactive.eai.transformer.message.MessageFactory;
/**
* BytesMessage 필드 validation 영향도 단위테스트
*
* 테스트 대상 레이아웃: EAI_UBYTESTYPE_REQ1 (BYTES 타입, 전문길이=35)
* - INT_VALUE : TYPE_BYTEARRAY(2113), length=5, decimal=0
* - BIG_VALUE : TYPE_BYTEARRAY(2113), length=10, decimal=5
* - STR_VALUE : TYPE_BYTEARRAY(2113), length=20, decimal=0
*
* 검증 목적
* BytesMessage 필드는 Converter.convert() TYPE_BYTEARRAY(2113) 분기를 사용하며,
* 변경 대상인 TYPE_BIGDECIMAL(2118) + String 분기와 완전히 분리되어 있음을 확인한다.
*
* 시나리오
* A) setData(byte[]) 경로 TYPE_BYTEARRAY 분기, 변경 전후 동일
* B) setString() 경로 TYPE_BYTEARRAY 분기, 변경 전후 동일
* B-1) 정상 숫자 문자열 예외 없음 (bytes로 저장)
* B-2) 비숫자 문자열 예외 없음 (bytes로 저장, BigDecimal 변환 없음)
* B-3) 자릿수 초과 문자열, validation=true 예외 없음 (TYPE_BYTEARRAY는 validation 대상 아님)
* B-4) 자릿수 초과 문자열, validation=false 예외 없음
*/
@DataJpaTest
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
@Sql({
"/com/eactive/eai/transformer/init_tseaitr06.sql",
"/com/eactive/eai/transformer/init_bytes_validation.sql"
})
@DisplayName("BytesMessage Validation 영향도 테스트")
class BytesMessageValidationTest {
@Autowired
TransformEngine engine;
// 전문 구조: INT_VALUE(5) + BIG_VALUE(10) + STR_VALUE(20) = 35 bytes
// BytesMessage는 소수점 없이 숫자만 저장 (decimal=5이면 5자리가 소수부)
private static final String VALID_RAW =
"00042" // INT_VALUE : 5자리 정수
+ "0012312345" // BIG_VALUE : 10자리 (정수부5 + 소수부5, 소수점 제외)
+ "HELLO WORLD "; // STR_VALUE : 20자리 문자열
@BeforeEach
void setUp() {
try {
if (!engine.isStarted()) {
engine.start();
}
} catch (Exception e) {
assumeTrue(false, "TransformEngine 시작 실패: " + e.getMessage());
}
}
@AfterEach
void tearDown() {
System.setProperty(SystemKeys.TRANSFROMER_VALIDATION, "false");
System.setProperty(SystemKeys.TRANSFROMER_SCIENTIFIC_NORMALIZE, "false");
SystemKeys.reload();
}
//
// A. setData(byte[]) 경로 TYPE_BYTEARRAY 분기 직접 사용
// Converter TYPE_BIGDECIMAL 분기와 완전히 무관
//
@Test
@DisplayName("[A] 정상 전문(byte[]) setData → 정상 처리")
void testSetDataWithValidBytes() {
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw),
"정상 전문 byte[] setData는 예외가 발생하지 않아야 합니다");
byte[] result = (byte[]) msg.getData();
assertNotNull(result, "getData() 결과가 null이 아니어야 합니다");
assertArrayEquals(raw, result, "입력 전문과 출력 전문이 동일해야 합니다");
}
@Test
@DisplayName("[A] validation=true 상태에서 byte[] setData → 영향 없음")
void testSetDataWithValidationEnabled() {
enableValidation();
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw),
"validation=true 설정이 BytesMessage setData(byte[])에 영향을 주지 않아야 합니다");
}
//
// B-1. setString() 정상 숫자 문자열
// TYPE_BYTEARRAY 분기 String.getBytes() 저장, BigDecimal 변환 없음
//
@Test
@DisplayName("[B-1] BIG_VALUE에 정상 숫자 문자열 setString → 예외 없음")
void testSetStringWithValidDecimal() {
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw));
assertDoesNotThrow(
() -> msg.setString("EAI_UBYTESTYPE_REQ1.BIG_VALUE[0]", "1234.12345"),
"정상 숫자 문자열 setString은 예외가 발생하지 않아야 합니다");
}
//
// B-2. setString() 비숫자 문자열
// TYPE_BYTEARRAY 분기 String.getBytes() 저장
// BigDecimal 변환이 없으므로 NumberFormatException 발생
// (JSONMessage와 다른 동작: JSONMessage는 Converter TYPE_BIGDECIMAL 분기 예외)
//
@Test
@DisplayName("[B-2] BIG_VALUE에 비숫자 문자열 setString → TYPE_BYTEARRAY이므로 예외 없음")
void testSetStringWithNonNumeric() {
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw));
assertDoesNotThrow(
() -> msg.setString("EAI_UBYTESTYPE_REQ1.BIG_VALUE[0]", "not-a-number"),
"TYPE_BYTEARRAY 필드는 비숫자 문자열도 bytes로 저장하므로 예외가 발생하지 않아야 합니다");
}
//
// B-3. setString() 자릿수 초과 문자열 + validation=true
// TYPE_BYTEARRAY 분기는 자릿수 검증 자체가 없음
// validation 설정과 무관하게 예외 발생
// 동작은 Converter TYPE_BIGDECIMAL 분기 수정 후에도 동일해야
//
@Test
@DisplayName("[B-3] BIG_VALUE 자릿수 초과 + validation=true → TYPE_BYTEARRAY이므로 예외 없음")
void testSetStringOverflowWithValidationEnabled() {
enableValidation();
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw));
// 정수부 5자리(12345) > BIG_VALUE decimal=5 기준 제한(4자리) 초과
// TYPE_BIGDECIMAL이었다면 validation 오류, TYPE_BYTEARRAY이므로 통과
assertDoesNotThrow(
() -> msg.setString("EAI_UBYTESTYPE_REQ1.BIG_VALUE[0]", "12345.123456"),
"TYPE_BYTEARRAY 필드는 validation 대상이 아니므로 자릿수 초과에도 예외가 발생하지 않아야 합니다");
}
//
// B-4. setString() 자릿수 초과 문자열 + validation=false
// validation 설정과 무관하게 예외 없음 (B-3과 결과 동일)
//
@Test
@DisplayName("[B-4] BIG_VALUE 자릿수 초과 + validation=false → 예외 없음")
void testSetStringOverflowWithValidationDisabled() {
// validation=false (기본값)
byte[] raw = VALID_RAW.getBytes();
Message msg = MessageFactory.getFactory().getMessage("EAI_UBYTESTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(raw));
assertDoesNotThrow(
() -> msg.setString("EAI_UBYTESTYPE_REQ1.BIG_VALUE[0]", "12345.123456"),
"validation=false(기본값) 상태에서 TYPE_BYTEARRAY 필드는 자릿수 초과에 예외가 발생하지 않아야 합니다");
}
//
// 유틸 메서드
//
private void enableValidation() {
System.setProperty(SystemKeys.TRANSFROMER_VALIDATION, "true");
SystemKeys.reload();
}
}
@@ -0,0 +1,164 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.nfunk.jep.JEP;
import com.eactive.eai.transformer.function.generic.ConcatN;
@DisplayName("ConcatN 가변 파라미터 함수 테스트")
class ConcatNFunctionTest {
private JEP jep;
@BeforeEach
void setUp() {
jep = new JEP();
jep.addStandardFunctions();
jep.addStandardConstants();
jep.setAllowUndeclared(true);
jep.addFunction("concatN", new ConcatN());
}
private String eval(String expr) throws Exception {
jep.parseExpression(expr);
assertFalse(jep.hasError(), "파싱 오류: " + jep.getErrorInfo());
Object result = jep.getValueAsObject();
assertFalse(jep.hasError(), "평가 오류: " + jep.getErrorInfo());
return (String) result;
}
//
// 파라미터 변화 테스트
//
@Test
@DisplayName("파라미터 1개 — 입력값 그대로 반환")
void testOneParam() throws Exception {
jep.addVariable("a", "Hello");
String result = eval("concatN(a)");
assertEquals("Hello", result);
}
@Test
@DisplayName("파라미터 2개 — 기존 Concat 동작과 동일")
void testTwoParams() throws Exception {
jep.addVariable("a", "Hello");
jep.addVariable("b", "World");
String result = eval("concatN(a, b)");
assertEquals("HelloWorld", result);
}
@Test
@DisplayName("파라미터 3개")
void testThreeParams() throws Exception {
jep.addVariable("a", "2024");
jep.addVariable("sep", "-");
jep.addVariable("b", "01");
String result = eval("concatN(a, sep, b)");
assertEquals("2024-01", result);
}
@Test
@DisplayName("파라미터 5개 — 날짜 + 시간 조합")
void testFiveParams() throws Exception {
jep.addVariable("year", "2024");
jep.addVariable("dash", "-");
jep.addVariable("month", "05");
jep.addVariable("dash2", "-");
jep.addVariable("day", "08");
String result = eval("concatN(year, dash, month, dash2, day)");
assertEquals("2024-05-08", result);
}
@Test
@DisplayName("파라미터 7개 — 날짜 + 공백 + 시간 조합")
void testSevenParams() throws Exception {
jep.addVariable("year", "2024");
jep.addVariable("dash", "-");
jep.addVariable("month", "05");
jep.addVariable("day", "08");
jep.addVariable("space", " ");
jep.addVariable("hour", "14");
jep.addVariable("minute", "30");
String result = eval("concatN(year, dash, month, dash, day, space, hour)");
assertEquals("2024-05-08 14", result);
}
//
// 변수 재할당 동일 표현식 재평가 파라미터 유지, 값만 변경
//
@Test
@DisplayName("동일 표현식(3개), 변수 값만 변경하여 반복 평가")
void testReuseExpressionWithDifferentValues() throws Exception {
jep.parseExpression("concatN(code, dash, seq)");
assertFalse(jep.hasError());
jep.addVariable("code", "KOR");
jep.addVariable("dash", "-");
jep.addVariable("seq", "001");
assertEquals("KOR-001", (String) jep.getValueAsObject());
jep.addVariable("code", "USA");
jep.addVariable("seq", "999");
assertEquals("USA-999", (String) jep.getValueAsObject());
jep.addVariable("code", "JPN");
jep.addVariable("seq", "042");
assertEquals("JPN-042", (String) jep.getValueAsObject());
}
//
// 리터럴 / 혼합 사용
//
@Test
@DisplayName("리터럴 문자열만 5개 사용")
void testLiteralsOnly() throws Exception {
String result = eval("concatN(\"A\", \"B\", \"C\", \"D\", \"E\")");
assertEquals("ABCDE", result);
}
@Test
@DisplayName("변수와 리터럴 혼합 — 괄호로 감싸기")
void testMixedVariableAndLiteral() throws Exception {
jep.addVariable("code", "KOR");
jep.addVariable("seq", "001");
String result = eval("concatN(\"[\", code, \"-\", seq, \"]\")");
assertEquals("[KOR-001]", result);
}
//
// 오류 케이스
//
@Test
@DisplayName("비문자열(숫자) 파라미터 포함 → 오류 발생")
void testNonStringParamCausesError() {
// JEP 정수 리터럴을 Double 처리하므로 평가 오류가 발생해야
jep.parseExpression("concatN(\"A\", 123, \"C\")");
if (!jep.hasError()) {
jep.getValueAsObject();
assertTrue(jep.hasError(), "비문자열 파라미터가 포함된 경우 오류가 발생해야 합니다");
}
}
}
@@ -0,0 +1,152 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.eactive.eai.transformer.engine.Parser;
import com.eactive.eai.transformer.function.generic.ConcatN;
/**
* ParserTest 스타일로 Parser 통해 concatN 함수를 검증한다.
*
* - 표현식은 parse() 변수값만 교체하며 반복 평가 (JEP 재사용 패턴)
* - 변수 : parser.addVariable() 바인딩
* - 상수 : 표현식 안에 "문자열" 리터럴로 직접 기입
*/
@DisplayName("ConcatN - Parser 변수/상수 혼합 테스트")
class ConcatNParserTest {
private Parser parser;
@BeforeEach
void setUp() {
parser = new Parser();
// DB 등록 테스트이므로 직접 등록
parser.getJep().addFunction("concatn", new ConcatN());
}
//
// 1. 변수만 사용
//
@Test
@DisplayName("변수 2개 — parse 1회, 값 교체 반복 평가")
void testTwoVariables() throws Exception {
parser.parse("concatn(a, b)");
parser.addVariable("a", "Hello");
parser.addVariable("b", "World");
assertEquals("HelloWorld", parser.getValueAsObject());
parser.addVariable("a", "foo");
parser.addVariable("b", "bar");
assertEquals("foobar", parser.getValueAsObject());
}
@Test
@DisplayName("변수 3개")
void testThreeVariables() throws Exception {
parser.parse("concatn(v1, v2, v3)");
parser.addVariable("v1", "AAA");
parser.addVariable("v2", "BBB");
parser.addVariable("v3", "CCC");
assertEquals("AAABBBCCC", parser.getValueAsObject());
}
@Test
@DisplayName("변수 5개")
void testFiveVariables() throws Exception {
parser.parse("concatn(y, m, d, h, mi)");
parser.addVariable("y", "2024");
parser.addVariable("m", "05");
parser.addVariable("d", "08");
parser.addVariable("h", "14");
parser.addVariable("mi", "30");
assertEquals("20240508" + "14" + "30", parser.getValueAsObject());
}
//
// 2. 변수 + 상수(리터럴) 혼합
//
@Test
@DisplayName("날짜 구분자 '-' 상수 삽입 — 변수 교체 반복")
void testDateWithSeparator() throws Exception {
parser.parse("concatn(year, \"-\", month, \"-\", day)");
parser.addVariable("year", "2024");
parser.addVariable("month", "01");
parser.addVariable("day", "15");
assertEquals("2024-01-15", parser.getValueAsObject());
parser.addVariable("year", "2025");
parser.addVariable("month", "12");
parser.addVariable("day", "31");
assertEquals("2025-12-31", parser.getValueAsObject());
}
@Test
@DisplayName("접두·접미 상수로 감싸기")
void testWrapWithConstants() throws Exception {
parser.parse("concatn(\"[\", code, \"-\", seq, \"]\")");
parser.addVariable("code", "KOR");
parser.addVariable("seq", "001");
assertEquals("[KOR-001]", parser.getValueAsObject());
parser.addVariable("code", "USA");
parser.addVariable("seq", "999");
assertEquals("[USA-999]", parser.getValueAsObject());
}
@Test
@DisplayName("앞에 상수 접두어 + 변수 1개")
void testConstantPrefix() throws Exception {
parser.parse("concatn(\"REQ-\", txId)");
parser.addVariable("txId", "20240508001");
assertEquals("REQ-20240508001", parser.getValueAsObject());
parser.addVariable("txId", "20240508002");
assertEquals("REQ-20240508002", parser.getValueAsObject());
}
@Test
@DisplayName("변수 사이에 공백 상수 삽입")
void testSpaceSeparator() throws Exception {
parser.parse("concatn(firstName, \" \", lastName)");
parser.addVariable("firstName", "Gil-dong");
parser.addVariable("lastName", "Hong");
assertEquals("Gil-dong Hong", parser.getValueAsObject());
}
//
// 3. 성능 측정 (ParserTest 스타일)
//
@Test
@DisplayName("100만 회 반복 평가 — parse 1회 재사용 성능")
void testPerformance() throws Exception {
String expr = "concatn(code, \"-\", seq)";
parser.parse(expr);
Object result = null;
long t = System.currentTimeMillis();
for (int i = 0; i < 1_000_000; i++) {
parser.addVariable("code", "KOR");
parser.addVariable("seq", String.valueOf(i));
result = parser.getValueAsObject();
}
long elapsed = System.currentTimeMillis() - t;
System.out.println(String.format("%s = %s %dms", expr, result, elapsed));
assertEquals("KOR-999999", result);
}
}
@@ -28,11 +28,16 @@ import com.eactive.eai.transformer.message.MessageSetDataException;
* - LONG_VALUE : LONG, length=10, decimal=0
* - BIG_VALUE : BIGDECIMAL, length=10, decimal=5 정수부 최대 4자리
* - BIG15_VALUE : BIGDECIMAL, length=15, decimal=0 정수부 최대 15자리
* - STR_VALUE : STRING, length=20
*
* 검증 시나리오
* 1) 타입 불일치 숫자형 필드에 비숫자 문자열 입력 setData() 에서 즉시 오류
* 2) 길이 초과 BigDecimal 정수부/소수부 자릿수 초과 setData() 에서 즉시 오류
* (SystemKeys.TRANSFROMER_VALIDATION=true 필요)
* 1-a) 타입 불일치(오류) 숫자형 필드에 비숫자 문자열 입력 setData() 에서 즉시 오류
* 1-b) 타입 불일치(정상) 숫자형 필드에 숫자 문자열 입력 파싱 가능하면 정상 처리
* 2-a) Java 타입 범위 초과 INT/LONG 최대값 초과 NumberFormatException (validation 설정 무관)
* 2-b) BigDecimal 자릿수 초과 (숫자 리터럴) 정수부/소수부 자릿수 초과 즉시 오류
* 2-c) BigDecimal 자릿수 초과 (문자열 형태) JSON 문자열로 전달된 경우도 동일하게 검증
* (2-b, 2-c 모두 SystemKeys.TRANSFROMER_VALIDATION=true 필요)
* 3) 역방향 타입 문자열 필드에 JSON 숫자 입력 asText() 자동 변환 오류 없음
*/
@DataJpaTest
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
@@ -60,6 +65,7 @@ class JSONMessageValidationTest {
@AfterEach
void tearDown() {
System.setProperty(SystemKeys.TRANSFROMER_VALIDATION, "false");
System.setProperty(SystemKeys.TRANSFROMER_SCIENTIFIC_NORMALIZE, "false");
SystemKeys.reload();
}
@@ -110,8 +116,82 @@ class JSONMessageValidationTest {
}
//
// 2. 숫자형 길이 초과 테스트 setData() 시점에 오류 발생
// transformer.validation=true 설정 필요
// 1-b. 숫자형 필드에 숫자 문자열 입력 정상 처리
// JSON 문자열 노드라도 파싱 가능한 숫자면 오류 없이 처리됨
//
@Test
@DisplayName("INT 타입 필드에 숫자 문자열 입력 → 오류 없이 정상 처리")
void testIntFieldWithNumericString() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"INT_VALUE\":\"42\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("\"INT_VALUE\":42"), "INT_VALUE가 숫자 42로 출력되어야 합니다");
}
@Test
@DisplayName("LONG 타입 필드에 숫자 문자열 입력 → 오류 없이 정상 처리")
void testLongFieldWithNumericString() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"LONG_VALUE\":\"9876543210\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("\"LONG_VALUE\":9876543210"), "LONG_VALUE가 숫자 9876543210으로 출력되어야 합니다");
}
@Test
@DisplayName("BIGDECIMAL 타입 필드에 숫자 문자열 입력 → 오류 없이 정상 처리")
void testBigDecimalFieldWithNumericString() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG_VALUE\":\"1234.12345\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("\"BIG_VALUE\":1234.12345"), "BIG_VALUE가 숫자 1234.12345로 출력되어야 합니다");
}
//
// 2-a. INT/LONG Java 타입 범위 초과 validation 설정 무관하게 NumberFormatException
//
@Test
@DisplayName("INT 타입 필드에 Integer 최대값 초과 입력 → NumberFormatException")
void testIntFieldJavaTypeOverflow() {
// Integer.MAX_VALUE = 2147483647, 초과값 new Integer() 파싱 실패
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"INT_VALUE\":\"2147483648\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsCause(ex, NumberFormatException.class),
"원인이 NumberFormatException이어야 합니다");
}
@Test
@DisplayName("LONG 타입 필드에 Long 최대값 초과 입력 → NumberFormatException")
void testLongFieldJavaTypeOverflow() {
// Long.MAX_VALUE = 9223372036854775807, 초과값 new Long() 파싱 실패
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"LONG_VALUE\":\"9223372036854775808\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsCause(ex, NumberFormatException.class),
"원인이 NumberFormatException이어야 합니다");
}
//
// 2-b. BigDecimal 자릿수 초과 (JSON 숫자 리터럴) transformer.validation=true 필요
//
@Test
@@ -180,6 +260,177 @@ class JSONMessageValidationTest {
assertTrue(result.contains("BIG_VALUE"), "BIG_VALUE가 결과에 포함되어야 합니다");
}
//
// 2-c. BigDecimal 자릿수 초과 (JSON 문자열 형태) transformer.validation=true 필요
//
@Test
@DisplayName("BIG_VALUE 정수부 초과 문자열 입력(5자리 > 4자리 제한) → MessageSetDataException")
void testBigDecimalIntegerPartOverflowFromString() {
enableValidation();
// JSON 문자열 "12345.12345" 정수부 5자리 초과
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG_VALUE\":\"12345.12345\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsMessage(ex, "Invalid decimal point format"),
"'Invalid decimal point format' 메시지가 포함되어야 합니다");
}
@Test
@DisplayName("BIG_VALUE 소수부 초과 문자열 입력(6자리 > 5자리 제한) → MessageSetDataException")
void testBigDecimalFractionalPartOverflowFromString() {
enableValidation();
// JSON 문자열 "1234.123456" 소수부 6자리 초과
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG_VALUE\":\"1234.123456\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsMessage(ex, "Invalid decimal point format"),
"'Invalid decimal point format' 메시지가 포함되어야 합니다");
}
@Test
@DisplayName("BIG15_VALUE 정수부 초과 문자열 입력(16자리 > 15자리 제한) → MessageSetDataException")
void testBigDecimalIntegerOnlyOverflowFromString() {
enableValidation();
// JSON 문자열 "1234567890123456" 정수부 16자리 초과
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG15_VALUE\":\"1234567890123456\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsMessage(ex, "Invalid decimal point format"),
"'Invalid decimal point format' 메시지가 포함되어야 합니다");
}
//
// 3. 역방향 타입 테스트 문자열 필드에 숫자 입력 정상 처리
// JSON 숫자 노드는 node.asText() 문자열 변환되어 오류 없이 처리됨
//
@Test
@DisplayName("STRING 타입 필드에 JSON 정수 입력 → 오류 없이 정상 처리")
void testStringFieldWithIntegerValue() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"STR_VALUE\":123}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("STR_VALUE"), "STR_VALUE가 결과에 포함되어야 합니다");
}
@Test
@DisplayName("STRING 타입 필드에 JSON 실수 입력 → 오류 없이 정상 처리")
void testStringFieldWithDecimalValue() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"STR_VALUE\":12.34}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("STR_VALUE"), "STR_VALUE가 결과에 포함되어야 합니다");
}
@Test
@DisplayName("STRING 타입 필드에 JSON 큰 정수 입력 → 오류 없이 정상 처리")
void testStringFieldWithLargeIntegerValue() throws Exception {
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"STR_VALUE\":9876543210}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("STR_VALUE"), "STR_VALUE가 결과에 포함되어야 합니다");
}
//
// 4. 지수 표현(Scientific Notation) 정규화 테스트
// transformer.scientific.normalize=true 때만 적용
// 4-a) 정규화 ON + validation ON
// 4-b) 정규화 OFF 기존 동작(false positive / false negative) 확인
//
@Test
@DisplayName("[정규화 ON] 지수형 정수부 초과(1.23e5=123000 > 4자리 제한) → MessageSetDataException")
void testScientificNotationIntegerOverflow_withNormalize() {
enableValidation();
enableScientificNormalize();
// 1.23e5 = 123000, 정수부 6자리 > 4자리 제한
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG_VALUE\":1.23e5}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsMessage(ex, "Invalid decimal point format"),
"'Invalid decimal point format' 메시지가 포함되어야 합니다");
}
@Test
@DisplayName("[정규화 ON] 지수형 문자열 정수부 초과(\"1.23e5\" > 4자리 제한) → MessageSetDataException")
void testScientificNotationStringOverflow_withNormalize() {
enableValidation();
enableScientificNormalize();
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG_VALUE\":\"1.23e5\"}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
MessageSetDataException ex = assertThrows(
MessageSetDataException.class,
() -> msg.setData(data)
);
assertTrue(containsMessage(ex, "Invalid decimal point format"),
"'Invalid decimal point format' 메시지가 포함되어야 합니다");
}
@Test
@DisplayName("[정규화 ON] 지수형 BIG15_VALUE 정상 범위(1.23e5=123000 ≤ 15자리) → 정상 처리")
void testScientificNotationBig15Valid_withNormalize() throws Exception {
enableValidation();
enableScientificNormalize();
// 1.23e5 = 123000, 6자리 15자리 제한 정상
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG15_VALUE\":1.23e5}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
msg.setData(data);
String result = (String) msg.getData();
assertNotNull(result, "결과값이 null이 아니어야 합니다");
assertTrue(result.contains("BIG15_VALUE"), "BIG15_VALUE가 결과에 포함되어야 합니다");
}
@Test
@DisplayName("[정규화 OFF] 지수형 BIG15_VALUE 정상값(1.23e5)이 false negative로 거부됨 → MessageSetDataException")
void testScientificNotationBig15Valid_withoutNormalize() {
enableValidation();
disableScientificNormalize();
// 1.23e5 = 123000, 유효값(6자리 15자리)이지만 정규화 없이는 "1.23E+5".split(".")=[2개] 거부
String data = "{\"EAI_UJSONTYPE_REQ1\":{\"BIG15_VALUE\":1.23e5}}";
Message msg = MessageFactory.getFactory().getMessage("EAI_UJSONTYPE_REQ1");
assertThrows(
MessageSetDataException.class,
() -> msg.setData(data),
"정규화 OFF 시 false negative로 예외가 발생해야 합니다"
);
}
//
// 유틸 메서드
//
@@ -189,6 +440,16 @@ class JSONMessageValidationTest {
SystemKeys.reload();
}
private void enableScientificNormalize() {
System.setProperty(SystemKeys.TRANSFROMER_SCIENTIFIC_NORMALIZE, "true");
SystemKeys.reload();
}
private void disableScientificNormalize() {
System.setProperty(SystemKeys.TRANSFROMER_SCIENTIFIC_NORMALIZE, "false");
SystemKeys.reload();
}
private boolean containsCause(Throwable e, Class<?> causeClass) {
Throwable t = e;
while (t != null) {
@@ -0,0 +1,253 @@
package com.eactive.eai.transformer;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.jdbc.Sql;
import com.eactive.eai.transformer.engine.TransformEngine;
import com.eactive.eai.transformer.function.jpa.TransformJPATestConfiguration;
import com.eactive.eai.transformer.message.Message;
import com.eactive.eai.transformer.message.MessageFactory;
import com.eactive.eai.transformer.message.MessageSetDataException;
/**
* XMLMessage BIGDECIMAL validation 단위테스트
*
* 테스트 대상 레이아웃: EAI_UXMLTYPE_REQ1 (XML 타입)
* - BIG_VALUE : TYPE_BIGDECIMAL(2118), length=10, decimal=5 정수부 최대 4자리
* - BIG15_VALUE : TYPE_BIGDECIMAL(2118), length=15, decimal=0 정수부 최대 15자리
* - STR_VALUE : TYPE_STRING(2101), length=20
*
* 검증 흐름
* setData(xmlString) doSetData() setValue(Element) doSetValue()
* TYPE_BIGDECIMAL 필드 + validation=true + 자릿수 초과 InvalidAccessException
* doSetData() catch MessageSetDataException
*
* 시나리오
* A) 정상 XML 예외 없음
* B) 정상 XML + validation=true 예외 없음
* C-1) BIG_VALUE 정수부 초과(5자리) + validation=true MessageSetDataException
* C-2) BIG_VALUE 정수부 초과(5자리) + validation=false 예외 없음
* D-1) BIG15_VALUE 소수점 포함(decimal=0 필드) + validation=true MessageSetDataException
* D-2) BIG15_VALUE 정수부 초과(16자리) + validation=true MessageSetDataException
* E) BIG15_VALUE 정상 정수(15자리) + validation=true 예외 없음
*/
@DataJpaTest
@ContextConfiguration(classes = { TransformJPATestConfiguration.class })
@Sql({
"/com/eactive/eai/transformer/init_tseaitr06.sql",
"/com/eactive/eai/transformer/init_xml_validation.sql"
})
@DisplayName("XMLMessage BIGDECIMAL Validation 테스트")
class XMLMessageValidationTest {
@Autowired
TransformEngine engine;
// BIG_VALUE: length=10, decimal=5 정수부 최대 4자리, 소수부 최대 5자리
// BIG15_VALUE: length=15, decimal=0 정수부 최대 15자리, 소수점 없음
private static final String VALID_XML =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE>1234.12345</BIG_VALUE>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
@BeforeEach
void setUp() {
try {
if (!engine.isStarted()) {
engine.start();
}
} catch (Exception e) {
assumeTrue(false, "TransformEngine 시작 실패: " + e.getMessage());
}
}
@AfterEach
void tearDown() {
System.setProperty(SystemKeys.TRANSFROMER_VALIDATION, "false");
System.setProperty(SystemKeys.TRANSFROMER_SCIENTIFIC_NORMALIZE, "false");
SystemKeys.reload();
}
//
// A. 정상 XML validation 설정 무관
//
@Test
@DisplayName("[A] 정상 XML → 예외 없음")
void testValidXml() {
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(VALID_XML),
"정상 XML 파싱은 예외가 발생하지 않아야 합니다");
}
@Test
@DisplayName("[B] 정상 XML + validation=true → 예외 없음")
void testValidXml_withValidation() {
enableValidation();
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(VALID_XML),
"정상값은 validation=true에서도 예외가 발생하지 않아야 합니다");
}
//
// C. BIG_VALUE 정수부 초과 validation 설정에 따른 동작 분기
// BIG_VALUE: length=10, decimal=5 정수부 최대 4자리
// "12345.12345" 정수부 5자리 초과
//
@Test
@DisplayName("[C-1] BIG_VALUE 정수부 초과(5자리) + validation=true → MessageSetDataException")
void testBigValueIntegerOverflow_withValidation() {
enableValidation();
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE>12345.12345</BIG_VALUE>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertThrows(MessageSetDataException.class, () -> msg.setData(xml),
"정수부 자릿수 초과 시 validation=true이면 MessageSetDataException이 발생해야 합니다");
}
@Test
@DisplayName("[C-2] BIG_VALUE 정수부 초과(5자리) + validation=false → 예외 없음")
void testBigValueIntegerOverflow_withoutValidation() {
// validation=false (기본값)
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE>12345.12345</BIG_VALUE>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(xml),
"validation=false(기본값) 상태에서는 자릿수 초과에도 예외가 발생하지 않아야 합니다");
}
//
// D. BIG15_VALUE (decimal=0 필드) 오류 케이스
// D-1) 소수점 포함값 decimal=0은 정수 전용, 소수점 자체가 오류
// D-2) 정수부 초과 16자리 > 15자리 제한
//
@Test
@DisplayName("[D-1] BIG15_VALUE decimal=0 필드에 소수점 포함값 + validation=true → MessageSetDataException")
void testBig15ValueWithDecimalPoint_withValidation() {
enableValidation();
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE>1234.12345</BIG_VALUE>" +
"<BIG15_VALUE>123.456</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertThrows(MessageSetDataException.class, () -> msg.setData(xml),
"decimal=0 필드에 소수점 포함값 입력 시 validation=true이면 MessageSetDataException이 발생해야 합니다");
}
@Test
@DisplayName("[D-2] BIG15_VALUE 정수부 초과(16자리) + validation=true → MessageSetDataException")
void testBig15ValueIntegerOverflow_withValidation() {
enableValidation();
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE>1234.12345</BIG_VALUE>" +
"<BIG15_VALUE>1234567890123456</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertThrows(MessageSetDataException.class, () -> msg.setData(xml),
"정수부 16자리(>15) 초과 시 validation=true이면 MessageSetDataException이 발생해야 합니다");
}
//
// E. BIG15_VALUE 정상 정수 (15자리) + validation=true 예외 없음
//
@Test
@DisplayName("[E] BIG15_VALUE 정상 정수(≤15자리) + validation=true → 예외 없음")
void testBig15ValueValidInteger_withValidation() {
enableValidation();
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(VALID_XML),
"15자리 이내 정수 입력 시 validation=true에서도 예외가 발생하지 않아야 합니다");
}
//
// F. 요소 / 요소 없음 경계 케이스
// F-1) 요소(<BIG_VALUE/>) + validation=true 예외 없음
// dom4j는 요소의 getData() null로 반환
// null instanceof String = false 검증 건너뜀 요소 없음과 동일 동작
// F-2) 요소(<BIG_VALUE/>) + validation=false 예외 없음
// F-3) 요소 없음(BIG_VALUE 미포함 XML) + validation=true 예외 없음
// XML에 요소가 없으면 doSetValue() 순회 대상이 없어 검증 자체가 실행되지 않음
//
@Test
@DisplayName("[F-1] BIG_VALUE 빈 요소(<BIG_VALUE/>) + validation=true → 예외 없음 (dom4j null 반환)")
void testBigValueEmptyElement_withValidation() {
enableValidation();
// dom4j는 요소의 getData() null로 반환 null instanceof String = false 검증 건너뜀
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE></BIG_VALUE>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(xml),
"dom4j 빈 요소는 null로 처리되어 검증 대상이 아니므로 예외가 발생하지 않아야 합니다");
}
@Test
@DisplayName("[F-2] BIG_VALUE 빈 문자열 + validation=false → 예외 없음")
void testBigValueEmptyString_withoutValidation() {
// validation=false (기본값)
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG_VALUE></BIG_VALUE>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(xml),
"validation=false(기본값) 상태에서는 빈 문자열도 예외가 발생하지 않아야 합니다");
}
@Test
@DisplayName("[F-3] BIG_VALUE 요소 없음 + validation=true → 예외 없음")
void testBigValueMissingElement_withValidation() {
enableValidation();
// BIG_VALUE 요소 자체를 XML에서 제외
String xml =
"<EAI_UXMLTYPE_REQ1>" +
"<BIG15_VALUE>123000</BIG15_VALUE>" +
"<STR_VALUE>HELLO</STR_VALUE>" +
"</EAI_UXMLTYPE_REQ1>";
Message msg = MessageFactory.getFactory().getMessage("EAI_UXMLTYPE_REQ1");
assertDoesNotThrow(() -> msg.setData(xml),
"XML에 요소가 없으면 검증 대상이 아니므로 예외가 발생하지 않아야 합니다");
}
//
// 유틸 메서드
//
private void enableValidation() {
System.setProperty(SystemKeys.TRANSFROMER_VALIDATION, "true");
SystemKeys.reload();
}
}
@@ -0,0 +1,17 @@
-- BytesMessage BIGDECIMAL validation 영향도 검증용 레이아웃
-- EAI_UBYTESTYPE_REQ1 : BYTES 타입, 전문길이=35 (5+10+20)
-- - INT_VALUE : TYPE_BYTEARRAY(2113), length=5, decimal=0 → loutitemrefptrnidname='Integer'
-- - BIG_VALUE : TYPE_BYTEARRAY(2113), length=10, decimal=5 → loutitemrefptrnidname='BigDecimal'
-- - STR_VALUE : TYPE_BYTEARRAY(2113), length=20, decimal=0 → loutitemrefptrnidname='String'
--
-- ※ BytesMessage 필드는 loutitemptrnidname=2113(TYPE_BYTEARRAY) 사용
-- Converter.convert()의 TYPE_BIGDECIMAL(2118) 분기와 완전히 분리됨을 확인하기 위한 레이아웃
INSERT INTO tseaitr07 (loutname,loutptrnname,loutdesc,lastamndhms,eaibzwkdstcd,uapplname,sysintfacname,eaisevrdstcd,modfimgtstusdstcd,useyn,verinfo,author) VALUES
('EAI_UBYTESTYPE_REQ1', 'BYTES', 'BYTES 타입 validation 영향도 검증', '2026052010000000', 'TST', ' ', ' ', 'E', ' ', '1', '00001', 'admin');
INSERT INTO TSEAITR08 (loutname,loutitemname,loutitemserno,loutitemidname,parntloutitemidname,loutitemdesc,loutitemnodeptrnidname,loutitemptrnidname,loutitempathname,loutitemlencnt,loutitemrefinfo,loutitemrefinfo2,loutitemoccurptrndstcd,loutitemmaxoccurnoitm,loutitemminoccurnoitm,loutitembascval,loutitemmskyn,loutdptcnt,decptlencnt,loutitemrefptrnidname,loutitemmasklength,multilang,loutitemmaskoffset) VALUES
('EAI_UBYTESTYPE_REQ1', 'EAI_UBYTESTYPE_REQ1', 0, 9010, 0, 'BYTES 타입 validation 검증', 2201, 2100, NULL, 0, NULL, NULL, NULL, 0, 0, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL),
('EAI_UBYTESTYPE_REQ1', 'INT_VALUE', 1, 9011, 9010, 'INT 타입 필드 (BYTEARRAY)', 2203, 2113, NULL, 5, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'Integer', 0, NULL, NULL),
('EAI_UBYTESTYPE_REQ1', 'BIG_VALUE', 2, 9012, 9010, 'BIGDECIMAL 필드 (BYTEARRAY) l=10 d=5', 2203, 2113, NULL, 10, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 5, 'BigDecimal', 0, NULL, NULL),
('EAI_UBYTESTYPE_REQ1', 'STR_VALUE', 3, 9013, 9010, 'STRING 타입 필드 (BYTEARRAY)', 2203, 2113, NULL, 20, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'String', 0, NULL, NULL);
@@ -6,4 +6,5 @@ INSERT INTO TSEAITR08 (loutname,loutitemname,loutitemserno,loutitemidname,parntl
('EAI_UJSONTYPE_REQ1', 'INT_VALUE', 1, 9001, 9000, 'INT 타입 필드', 2203, 2102, NULL, 5, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'Integer', 0, NULL, NULL),
('EAI_UJSONTYPE_REQ1', 'LONG_VALUE', 2, 9002, 9000, 'LONG 타입 필드', 2203, 2103, NULL, 10, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'Long', 0, NULL, NULL),
('EAI_UJSONTYPE_REQ1', 'BIG_VALUE', 3, 9003, 9000, 'BIGDECIMAL length=10 decimal=5', 2203, 2118, NULL, 10, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 5, 'BigDecimal', 0, NULL, NULL),
('EAI_UJSONTYPE_REQ1', 'BIG15_VALUE', 4, 9004, 9000, 'BIGDECIMAL length=15 decimal=0', 2203, 2118, NULL, 15, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'BigDecimal', 0, NULL, NULL);
('EAI_UJSONTYPE_REQ1', 'BIG15_VALUE', 4, 9004, 9000, 'BIGDECIMAL length=15 decimal=0', 2203, 2118, NULL, 15, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'BigDecimal', 0, NULL, NULL),
('EAI_UJSONTYPE_REQ1', 'STR_VALUE', 5, 9005, 9000, 'STRING 타입 필드', 2203, 2101, NULL, 20, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'String', 0, NULL, NULL);
@@ -0,0 +1,14 @@
-- XMLMessage BIGDECIMAL validation 테스트용 레이아웃
-- EAI_UXMLTYPE_REQ1 : XML 타입
-- - BIG_VALUE : TYPE_BIGDECIMAL(2118), length=10, decimal=5 → 정수부 최대 4자리
-- - BIG15_VALUE : TYPE_BIGDECIMAL(2118), length=15, decimal=0 → 정수부 최대 15자리, 소수점 없음
-- - STR_VALUE : TYPE_STRING(2101), length=20, decimal=0
INSERT INTO tseaitr07 (loutname,loutptrnname,loutdesc,lastamndhms,eaibzwkdstcd,uapplname,sysintfacname,eaisevrdstcd,modfimgtstusdstcd,useyn,verinfo,author) VALUES
('EAI_UXMLTYPE_REQ1', 'XML', 'XML 타입 BIGDECIMAL validation 테스트', '2026052010000000', 'TST', ' ', ' ', 'E', ' ', '1', '00001', 'admin');
INSERT INTO TSEAITR08 (loutname,loutitemname,loutitemserno,loutitemidname,parntloutitemidname,loutitemdesc,loutitemnodeptrnidname,loutitemptrnidname,loutitempathname,loutitemlencnt,loutitemrefinfo,loutitemrefinfo2,loutitemoccurptrndstcd,loutitemmaxoccurnoitm,loutitemminoccurnoitm,loutitembascval,loutitemmskyn,loutdptcnt,decptlencnt,loutitemrefptrnidname,loutitemmasklength,multilang,loutitemmaskoffset) VALUES
('EAI_UXMLTYPE_REQ1', 'EAI_UXMLTYPE_REQ1', 0, 9020, 0, 'XML BIGDECIMAL validation 루트', 2201, 2100, NULL, 0, NULL, NULL, NULL, 0, 0, NULL, NULL, 1, 0, NULL, NULL, NULL, NULL),
('EAI_UXMLTYPE_REQ1', 'BIG_VALUE', 1, 9021, 9020, 'BIGDECIMAL length=10 decimal=5', 2203, 2118, NULL, 10, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 5, 'BigDecimal', 0, NULL, NULL),
('EAI_UXMLTYPE_REQ1', 'BIG15_VALUE', 2, 9022, 9020, 'BIGDECIMAL length=15 decimal=0', 2203, 2118, NULL, 15, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'BigDecimal', 0, NULL, NULL),
('EAI_UXMLTYPE_REQ1', 'STR_VALUE', 3, 9023, 9020, 'STRING 타입 필드', 2203, 2101, NULL, 20, NULL, NULL, NULL, 1, 1, NULL, 'N', 2, 0, 'String', 0, NULL, NULL);