Compare commits

...

19 Commits

Author SHA1 Message Date
curry772 3f75bd9b49 QueryString(parameter, formdata) 관련 Util 클래스 추가 2026-08-10 15:25:30 +09:00
curry772 51d95d8824 QueryString(parameter, formdata) 관련 Util 클래스 추가 2026-08-10 15:25:30 +09:00
curry772 5c3de18cc9 HttpAdapterServiceVirtualAccount online -> common 이동 2026-08-06 14:23:09 +09:00
curry772 a5a072fef4 Merge branch 'master' of https://git.eactive.synology.me:8090/eapim/djbank/elink-online-common.git 2026-08-06 09:30:17 +09:00
curry772 078331b3b8 In 필터 개발 : 데이터부에 상태코드 값을 응답 HTTP 상태코드에 설정하는 필터 2026-08-06 09:29:54 +09:00
Rinjae 428ab1103a damo-manager.jar 라이브러리 업데이트
- eapim-admin, eapim-online, elink-portal 공통 반영
2026-08-05 17:46:54 +09:00
curry772 4813a2ef0e 거래고유번호 맨앞에 instid 추가 2026-08-03 13:29:17 +09:00
curry772 752156b86f 거래고유번호 맨앞에 instid 추가 2026-08-03 11:38:21 +09:00
curry772 5732ddf962 checkTokenRetry 메소드 refactoring 2026-07-31 14:26:45 +09:00
curry772 5ffc48c039 REST 전송 body log의 길이를 줄이는 문제 처리 2026-07-31 13:48:39 +09:00
curry772 cbb6f33ce5 HttpClient5AdapterServiceRest.checkTokenRetry private -> protected 2026-07-31 13:48:08 +09:00
curry772 44093681b7 AdapterErrorMessageHandler 프러퍼티 그룹에 default template 기능 추가
- default.template
- default.in.template
- default.sys.template
2026-07-31 09:35:48 +09:00
curry772 a24b437f5c 수신에서 표준오류응답을 수신하였는데 Inbound 어댑터에 AdapterErrorHandler 설정하였으나, 처리 후 null를
리턴하는 경우, 처리 로직 추가
2026-07-30 14:08:25 +09:00
curry772 67dba6ca4a R(응답)거래가 없어져서 소스 원복 2026-07-30 11:25:44 +09:00
curry772 f4d7ac2fad 타발응답거래(R2) 거래시 로그 출력 및 응답 안주는 문제 처리
- R2거래이어도 동기 거래이기 때문에 응답을 전달해야 함.
- 로그도 100 ~ 400을 출력해야 함.
2026-07-30 08:57:09 +09:00
curry772 6ca653aa1e oauth token scope 검증 중 통과 시킬 scope 배열 리스트 설정으로 변경
- oAuthServer pass.scope.list에 ',' 구분자로 통과시킬 scope 리스트
  설정(ex:public,oob)
2026-07-27 14:56:20 +09:00
curry772 90941bff54 DAMO 암호화 복호화 실패 시 원본 리턴되도록 수정 2026-07-27 10:43:33 +09:00
curry772 9e1f7391a0 ToolsController 개발
- base64 인코딩, damo 암복호화, version 정보
2026-07-23 11:43:59 +09:00
curry772 cb0a588a9c ToolsController(/manage/tools) Postman 컬렉션 추가
base64/hex 변환, DAMO 및 EncryptionManager 암복호화, 배포 버전 조회 API를
기존 CryptoModuleManage/SessionCacheManage 컬렉션과 동일한 스타일로 샘플링.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 09:10:27 +09:00
23 changed files with 3016 additions and 24 deletions
Binary file not shown.
+353
View File
@@ -0,0 +1,353 @@
{
"info": {
"_postman_id": "d4a8b2f0-5e3c-4d21-9f0a-tools-manage-0001",
"name": "ToolsManage API (/manage/tools)",
"description": "base64/hex 변환, DAMO 및 EncryptionManager 암복호화 테스트, 배포 버전 확인용 유틸리티 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. DAMO/EncryptionManager 암복호화는 각 실행 순서(암호화 → 복호화)를 지켜야 체이닝 변수가 채워짐\n3. EncryptionManager 는 encryptYN=N 환경에서는 encrypt/decrypt가 원문을 그대로 반환한다(정상 동작). 먼저 '상태 조회'로 확인할 것\n4. /manage/tools/version 은 eapim-online(WAR 루트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info 리소스가 있어야 success=true로 응답한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등에서는 success=false + message로 응답하며, 이는 정상 동작이다.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
{ "key": "charset", "value": "UTF-8", "type": "string" },
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
{ "key": "base64EncodedText", "value": "", "type": "string" },
{ "key": "hexEncodedText", "value": "", "type": "string" },
{ "key": "damoCipherText", "value": "", "type": "string" },
{ "key": "encManagerCipherText", "value": "", "type": "string" }
],
"item": [
{
"name": "1. Base64 / Hex 변환",
"item": [
{
"name": "문자열 → Base64 인코딩",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/base64/encode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "base64", "encode"]
},
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 Base64로 인코딩한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('base64EncodedText', json.data);"
]
}
}
]
},
{
"name": "Base64 → 문자열 디코딩 (위 인코딩 결과 사용)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{base64EncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/base64/decode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "base64", "decode"]
},
"description": "바로 위 '문자열 → Base64 인코딩' 요청에서 저장한 base64EncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
},
{
"name": "문자열 → Hex 인코딩",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/hex/encode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "hex", "encode"]
},
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 대문자 Hex 문자열로 인코딩한다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('hexEncodedText', json.data);"
]
}
}
]
},
{
"name": "Hex → 문자열 디코딩 (위 인코딩 결과 사용)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{hexEncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/hex/decode",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "hex", "decode"]
},
"description": "바로 위 '문자열 → Hex 인코딩' 요청에서 저장한 hexEncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "2. DAMO 암복호화 (DamoManager 직접 호출)",
"item": [
{
"name": "암호화 (encrypt)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/damo/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "damo", "encrypt"]
},
"description": "com.eactive.ext.djb.DamoManager를 직접 호출한다. EncryptionManager의 encryptYN 설정과 무관하게 항상 실제 암호화가 수행된다."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.test('평문과 달라야 함', () => pm.expect(json.data).to.not.eql(pm.collectionVariables.get('plainText')));",
"pm.collectionVariables.set('damoCipherText', json.data);"
]
}
}
]
},
{
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{damoCipherText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/damo/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "damo", "decrypt"]
},
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 damoCipherText를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "3. EncryptionManager 암복호화 (운영 설정 반영)",
"item": [
{
"name": "현재 설정 상태 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/status",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "status"]
},
"description": "encryptYN/dbEncryptSolutionName/encryptEnabled를 조회한다. encryptYN=N이면 아래 encrypt/decrypt가 원문을 그대로 반환하는 것이 정상 동작이니 먼저 이 값을 확인할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('encryptYN 존재', () => pm.expect(json.data.encryptYN).to.exist);",
"pm.test('encryptEnabled는 boolean', () => pm.expect(json.data.encryptEnabled).to.be.a('boolean'));",
"console.log('EncryptionManager 상태:', JSON.stringify(json.data));"
]
}
}
]
},
{
"name": "암호화 (encrypt)",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/encrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "encrypt"]
},
"description": "실제 운영 코드가 사용하는 EncryptionManager.encryptDBData를 그대로 호출한다. encryptYN=N 환경에서는 원문이 그대로 반환된다(정상 동작)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
"pm.collectionVariables.set('encManagerCipherText', json.data);"
]
}
}
]
},
{
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
"request": {
"method": "POST",
"header": [{ "key": "Content-Type", "value": "application/json" }],
"body": {
"mode": "raw",
"raw": "{\n \"text\": \"{{encManagerCipherText}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/manage/tools/encryption-manager/decrypt",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "encryption-manager", "decrypt"]
},
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 encManagerCipherText를 복호화한다. encryptYN 설정값과 무관하게 encrypt→decrypt 라운드트립 결과는 항상 원문과 같아야 한다(활성 시 실제 복호화, 비활성 시 원문 그대로 통과). 반드시 암호화 요청을 먼저 실행할 것."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
]
}
}
]
}
]
},
{
"name": "4. 버전 정보",
"item": [
{
"name": "배포 버전 조회",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/manage/tools/version",
"host": ["{{baseUrl}}"],
"path": ["manage", "tools", "version"]
},
"description": "eapim-online(WAR를 만드는 루트 프로젝트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info(git describe 결과, buildTime)를 반환한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등 파일이 없으면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)."
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const json = pm.response.json();",
"pm.test('status 200', () => pm.response.to.have.status(200));",
"if (json.success) {",
" pm.test('version 존재', () => pm.expect(json.data.version).to.be.a('string'));",
" pm.test('buildTime 존재', () => pm.expect(json.data.buildTime).to.be.a('string'));",
"} else {",
" pm.test('미생성 환경은 message 포함', () => pm.expect(json.message).to.be.a('string'));",
" console.log('version.info 없음:', json.message);",
"}"
]
}
}
]
}
]
}
]
}
@@ -128,6 +128,9 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
String templateKey = inboundAdapterGroupName + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
if (StringUtils.isBlank(template)) {
if (logger.isWarn()) {
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
@@ -320,6 +323,8 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
? expr.substring(closeIdx + 2)
: "";
if (msg == null) return defaultVal;
String key = msg.findItemValue(msgPath);
if (StringUtils.isBlank(key)) return defaultVal;
if (callProp == null) return defaultVal;
@@ -409,6 +414,9 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
String templateKey = inboudnAdapterGroupName + ".sys.template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.sys.template");
if (StringUtils.isBlank(template)) {
if (logger.isWarn()) {
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
@@ -456,6 +464,10 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
throws Exception {
String templateKey = outboundadapterGroupName + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template))
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
if (StringUtils.isBlank(template)) {
return null;
}
@@ -466,15 +478,27 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
String adptMsgType, String encode, Throwable e1, int httpCode) {
String templateKey = adapterGroupName + ".in." + httpCode + ".template";
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
templateKey = "default.in." + httpCode + ".template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
}
if (StringUtils.isBlank(template)) {
templateKey = adapterGroupName + ".in.template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
if (StringUtils.isBlank(template)) {
String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT");
return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode,
MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat);
}
}
if (StringUtils.isBlank(template)) {
templateKey = "default.in.template";
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
}
if (StringUtils.isBlank(template)) {
String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT");
return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode,
MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat);
}
return render(template, null, callProp, e1);
}
@@ -527,8 +527,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
// 이경우 정상으로 처리함
} else if (status >= 200 && status <= 207) {
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
needReissue = checkTokenRetry(status, responseMessage, tokenErrorHttpStatusCode, tokenErrorCodeKey,
tokenErrorCodeValues, vo.getEncode());
}
} else if (status == 302) {
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
@@ -542,15 +542,14 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
if (status >= 400 && status < 500) {
if (useAdapterToken) {
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
vo.getEncode());
needReissue = checkTokenRetry(status, responseMessage, tokenErrorHttpStatusCode,
tokenErrorCodeKey, tokenErrorCodeValues, vo.getEncode());
}
if (!needReissue) {
throw new Exception(errMsg);
}
} else {
throw new Exception(errMsg);
}
}
@@ -882,8 +881,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
}
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
String encode) {
protected boolean checkTokenRetry(int status, byte[] responseMessage, String tokenErrorHttpStatusCode,
String tokenErrorCodeKey, String tokenErrorCodeValues, String encode) {
if (responseMessage == null) {
return false;
}
@@ -18,6 +18,7 @@ import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactory;
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.TxSiftContext;
import com.eactive.eai.common.util.UUIDGenerator;
@@ -31,13 +32,20 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
public static final String PROPERTIES_NAME_CLIENT_ID = "clientId";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private ObjectMapper mapper = new ObjectMapper();
EAIServerManager eaiServerManager;
String instid = null;
public Object service(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
boolean bMDCput = false;
if(uuid == null) {
uuid = UUIDGenerator.getUUID().toString().replaceAll("-", "");
if(instid == null) {
eaiServerManager = EAIServerManager.getInstance();
instid = eaiServerManager.getGroupInstId();
}
uuid = instid + UUIDGenerator.getUUID().toString().replaceAll("-", "");
prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid);
}
@@ -50,6 +50,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
public static final String PASS_SCOPE_LIST = "pass.scope.list";
public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
@@ -59,7 +60,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
private JWSVerifier jwsVerifier;
private String[] passScopeArr = {};
// // CA 토큰 저장소
// private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>();
@@ -89,6 +90,10 @@ public class ApiAuthFilter implements HttpAdapterFilter {
} catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) {
throw new RuntimeException(e);
}
String passScope = vo.getProperty(PASS_SCOPE_LIST, "oob,public");
passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
}
@Override
@@ -316,9 +321,6 @@ public class ApiAuthFilter implements HttpAdapterFilter {
return false;
}
String passScope = "oob,public";
String[] passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
for (String scope : scopeArr) {
for (String pScope : passScopeArr) {
if ( StringUtils.equalsAny(scope, pScope)) {
@@ -0,0 +1,67 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.util.JsonPathUtil;
import com.fasterxml.jackson.databind.JsonNode;
public class JsonToSetStatusFilter implements HttpAdapterFilter {
private static final String PROPGROUP = "JsonToSetStatusFilter";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
/** HTTP 상태코드 유효 범위 (RFC 7231) */
private static final int MIN_HTTP_STATUS = 100;
private static final int MAX_HTTP_STATUS = 599;
@Override
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// do nothing
return message;
}
@Override
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
HttpServletRequest request, HttpServletResponse response) throws Exception {
String fieldName = "";
String statusParam = "";
try {
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
fieldName = getFieldName(adptGrpName);
if (rootNode != null && StringUtils.isNotBlank(fieldName) && rootNode.has(fieldName)) {
statusParam = rootNode.get(fieldName).asText();
int httpStatus = Integer.parseInt(statusParam);
if (isValidHttpStatus(httpStatus)) {
response.setStatus(httpStatus);
} else {
logger.warn("유효하지 않은 HTTP 상태코드. 상태코드를 설정하지 않음. fieldName={}, value={}", fieldName, statusParam);
}
} else {
logger.warn("설정과 맞지 않는 메시지. 상태코드를 설정하지 않음. fieldName={}", fieldName);
}
} catch (Exception e) {
logger.warn("상태코드 추출 실패. fieldName={}, value={}", fieldName, statusParam, e);
}
return resultMessage;
}
/** HTTP 상태코드로 사용 가능한 값인지 확인한다. (100 ~ 599) */
private boolean isValidHttpStatus(int httpStatus) {
return httpStatus >= MIN_HTTP_STATUS && httpStatus <= MAX_HTTP_STATUS;
}
private String getFieldName(String adptGrpName) {
return PropManager.getInstance().getProperty(PROPGROUP, adptGrpName);
}
private JsonNode parseJson(String adptGrpName, Object message) throws Exception {
return JsonPathUtil.toTree(message);
}
}
@@ -0,0 +1,766 @@
package com.eactive.eai.adapter.http.dynamic.impl;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.apache.mina.common.ByteBuffer;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import com.eactive.eai.adapter.AdapterManager;
import com.eactive.eai.adapter.AdapterPropManager;
import com.eactive.eai.adapter.AdapterVO;
import com.eactive.eai.adapter.Keys;
import com.eactive.eai.adapter.http.HttpMemoryLogger;
import com.eactive.eai.adapter.http.HttpMethodType;
import com.eactive.eai.adapter.http.HttpStatusException;
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
import com.eactive.eai.common.TransactionContextKeys;
import com.eactive.eai.common.exception.ExceptionUtil;
import com.eactive.eai.common.message.MessageType;
import com.eactive.eai.common.util.CommonLib;
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
import com.eactive.eai.common.util.Logger;
import com.eactive.eai.common.util.MessageUtil;
import com.eactive.eai.env.ElinkConfig;
import com.eactive.eai.inbound.action.ActionFactory;
import com.eactive.eai.inbound.action.RequestAction;
import com.eactive.eai.inbound.processor.Processor;
import com.eactive.eai.message.StandardMessageUtil;
/*
* Kbank 가상계좌 INBOUND
* @see com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
* @Deprecated
*/
// FIXME : kbank - kbank에서는 Controller 방식을 사용하므로, 이 어댑터는 사용되지 않음 (VirtualAccountCryptoFilter 로 대체)
public class HttpAdapterServiceVirtualAccount extends HttpAdapterServiceSupport {
public static final String HEADER_GROUP = "HEADER_GROUP";
// HEADER_GROUP JSON에 추가할 항목 정의, 없으면 전체 header 추가
public static final String HEADER_KEYS = "HEADER_KEYS";
public static final String HTTP_STATUS = "HTTP_STATUS";
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
private String logPrefix = "HttpAdapterServiceRest] ";
private static final String JSON_CONTENT_TYPE = "application/json";
private static final String JSON_FIELD_NAME = "json-body";
private static final String FILE_GROUP_NAME = "image-file";
private static final String UPLOAD_ROOT_PATH = "UPLOAD_ROOT_PATH";
private Properties addCryptoFilter(Properties prop) {
String cryptoFilterName = "com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter";
String addedPreFilter = prop.getProperty(PRE_FILTERS);
String addedPostFilter = prop.getProperty(POST_FILTERS);
if(StringUtils.isBlank(addedPreFilter)) {
addedPreFilter = cryptoFilterName;
}
else {
addedPreFilter = addedPreFilter + "," +cryptoFilterName;
}
if(StringUtils.isBlank(addedPostFilter)) {
addedPostFilter = cryptoFilterName;
}
else {
addedPostFilter = cryptoFilterName + "," +addedPostFilter;
}
prop.setProperty(PRE_FILTERS, addedPreFilter);
prop.setProperty(POST_FILTERS, addedPostFilter);
return prop;
}
private String readMultipartBody(HttpServletRequest request) throws Exception {
String jsonString = null;
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Set the maximum size of the files to be uploaded
factory.setSizeThreshold(1024 * 1024);
// Set the temporary directory to store uploaded files
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
Map<String, String> fileMap = new HashMap<>();
InputStream fin = null;
try {
byte[] buffer = new byte[1024];
int read = 0;
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// file
String fieldName = item.getFieldName();
String fileName = item.getName();
fin = item.getInputStream();
ByteArrayOutputStream fo = new ByteArrayOutputStream();
while ((read = fin.read(buffer)) > 0) {
fo.write(buffer, 0, read);
}
int fileSize = fo.size();
byte[] fileBytes = fo.toByteArray();
String fileContents = new String(fileBytes);
if (logger.isInfo()) {
logger.info("[FILE]-------------------------------------------------->");
logger.info("Field name = " + fieldName);
logger.info("File name = " + fileName + " contents length = " + fileSize);
logger.info("File Contents [" + fileContents + "]");
logger.info("[FILE]<--------------------------------------------------");
}
fileMap.put(fileName, fileContents);
fin.close();
} else {
// regular form field
String fieldName = item.getFieldName();
String fieldValue = item.getString();
if (logger.isInfo()) {
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
}
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|| JSON_FIELD_NAME.equals(fieldName)) {
jsonString = fieldValue;
}
}
}
if (logger.isInfo()) {
logger.info("Json body [" + jsonString + "]");
}
if (jsonString == null) {
jsonString = "{}";
} else {
// parsing json & add file contents
JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonString);
if (jsonObject == null) {
jsonString = "{}";
} else {
JSONObject fileGroup = new JSONObject();
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
fileGroup.put("fileName", entry.getKey());
fileGroup.put("fileContents", entry.getValue());
}
jsonObject.put(FILE_GROUP_NAME, fileGroup);
jsonString = jsonObject.toJSONString();
}
}
if (logger.isInfo()) {
logger.info("Json with file [" + jsonString + "]");
}
return jsonString;
} catch (Exception e) {
logger.error("Read multipart body error.", e);
throw e;
} finally {
if (fin != null) {
try {
fin.close();
} catch (Exception ex) {
// empty
}
}
}
}
private String checkRootPath(String path) {
if (StringUtils.isEmpty(path)) {
logger.info("upload dir not set(UPLOAD_ROOT_PATH), use system temp " + path);
return System.getProperty("java.io.tmpdir");
}
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
return path;
}
private String uploadMultipartBody(HttpServletRequest request, String uploadRootPath) throws Exception {
String jsonString = null;
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Set the maximum size of the files to be uploaded
factory.setSizeThreshold(1024 * 1024);
// Set the temporary directory to store uploaded files
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
factory.setRepository(tempDir);
ServletFileUpload upload = new ServletFileUpload(factory);
try {
// if not exist, create folders
String uploadDir = checkRootPath(uploadRootPath);
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) {
// file
String fieldName = item.getFieldName();
String fileName = item.getName();
String uploadFilePath = uploadDir + File.separator + fileName;
File uploadFile = new File(uploadFilePath);
item.write(uploadFile);
if (logger.isInfo()) {
logger.info("[FILE]-------------------------------------------------->");
logger.info("Field name = " + fieldName);
logger.info("File name = " + fileName + " path = " + uploadFile.getAbsolutePath());
logger.info("[FILE]<--------------------------------------------------");
}
} else {
// regular form field
String fieldName = item.getFieldName();
String fieldValue = item.getString();
if (logger.isInfo()) {
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
}
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|| JSON_FIELD_NAME.equals(fieldName)) {
jsonString = fieldValue;
}
}
}
if (logger.isInfo()) {
logger.info("Json body [" + jsonString + "]");
}
return jsonString;
} catch (Exception e) {
logger.error("Read multipart body error.", e);
throw e;
}
}
@SuppressWarnings({ "unchecked", "deprecation" })
public void service(String adptGrpName, String adptName, HttpServletRequest request, HttpServletResponse response) {
int traceLevel = 0;
AdapterVO adptVO = null;
AdapterPropManager manager = null;
Properties httpProp = null;
String responseType = null;
String urlDecodeYn = null;
String encode = null;
String traceLevelTemp = null;
String relayRequestHeaderKeys = null;
String headerGroupName = null;
boolean isParameterType = false;
String message = null;
StopWatch stopWatch = null;
Properties prop = null;
String paramValue = null;
String adptMsgType = null;
String errorResponseFormat = null;
String uploadRootPath = null;
try {
AdapterManager adapterManager = AdapterManager.getInstance();
adptVO = adapterManager.getAdapterVO(adptGrpName, adptName);
if (adptVO == null) {
throw new Exception("Adapter not found error");
}
manager = AdapterPropManager.getInstance();
httpProp = manager.getProperties(adptVO.getPropGroupName());
responseType = httpProp.getProperty(RESPONSE_TYPE, "SYNC");
urlDecodeYn = httpProp.getProperty(URL_DECODE_YN, "N");
// encode = httpProp.getProperty(ENCODE, "UTF-8");
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(),
"UTF-8");
traceLevelTemp = httpProp.getProperty(TRACE_LEVEL, "0");
relayRequestHeaderKeys = httpProp.getProperty(HEADER_KEYS);
headerGroupName = httpProp.getProperty(HEADER_GROUP);
errorResponseFormat = httpProp.getProperty(ERROR_RESPONSE_FORMAT);
uploadRootPath = httpProp.getProperty(UPLOAD_ROOT_PATH);
prop = new Properties();
prop.put(INBOUND_METHOD, request.getMethod());
prop.put(INBOUND_URI, request.getRequestURI());
prop.put(INBOUND_HEADER, getHeaders(request));
prop.put(INBOUND_EXTPARAMS, StringUtils.defaultString(request.getQueryString()));
if (StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_REST)
|| StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_HTTP_CUSTOM)) {
// /api/v1/public/getUserInfo.svc
String extUrl = StringUtils.removeStart(request.getRequestURI(), request.getContextPath());
prop.put(INBOUND_EXTURI, extUrl);
} else {
prop.put(INBOUND_EXTURI, getExtUri(request));
}
prop.put(Processor.REQUEST_ACTION, adptVO.getAdapterGroupVO().getRefClass());
prop.put(API_PATH, httpProp.getProperty(API_PATH, ""));
prop.put(PRE_FILTERS, httpProp.getProperty(PRE_FILTERS, ""));
prop.put(POST_FILTERS, httpProp.getProperty(POST_FILTERS, ""));
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
isParameterType = false;
try {
traceLevel = Integer.parseInt(traceLevelTemp);
} catch (Exception e) {
traceLevel = 0;
}
stopWatch = new StopWatch();
stopWatch.start();
logger.debug("시작 >> encode = [" + encode + "]");
switch (HttpMethodType.getValue(request.getMethod())) {
case GET:
case DELETE:
isParameterType = true;
break;
case POST:
case PUT:
if (StringUtils.contains(request.getContentType(), "application/x-www-form-urlencoded")) {
isParameterType = true;
} else {
isParameterType = false;
}
break;
default:
break;
}
if (isParameterType) {
paramValue = request.getQueryString();
if (paramValue == null)
paramValue = "";
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
}
// json으로 변환
StringBuilder sb = new StringBuilder();
sb.append("{");
Map<String, String[]> paramMap = assignParameterMap(request, adptGrpName, adptName, null, prop);
int i = 0;
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
if (i > 0) {
sb.append(",");
}
sb.append("\"").append(entry.getKey()).append("\":");
String[] values = entry.getValue();
if (values.length > 1) {
// ["111", "222"]
sb.append("[");
for (int j = 0; j < values.length; j++) {
if (j > 0) {
sb.append(",");
}
sb.append("\"").append(JSONValue.escape(values[j])).append("\"");
}
sb.append("]");
} else {
sb.append("\"").append(JSONValue.escape(values[0])).append("\"");
}
i++;
}
sb.append("}");
paramValue = sb.toString();
} else {
if (ServletFileUpload.isMultipartContent(request)) {
// TODO : 아래의 로직은 업무에 맞게 수정이 필요함.
// 불필요할 경우 제거
// if(StringUtils.isEmpty(uploadRootPath)) {
// uploadRootPath = System.getProperty("java.io.tmpdir");
// }
// 임시로직 : UPLOAD_ROOT_PATH 가 없는 경우에는 JSON에 추가
if (StringUtils.isEmpty(uploadRootPath)) {
paramValue = readMultipartBody(request);
} else {
paramValue = uploadMultipartBody(request, uploadRootPath);
}
// TEST : 테스트용 임시코드
// response.setCharacterEncoding(encode);
// response.getWriter().print(paramValue);
// return;
} else {
ServletInputStream sis = request.getInputStream();
ByteBuffer bb = ByteBuffer.allocate(1024).setAutoExpand(true);
int i = 0;
byte[] cbuf = new byte[1024];
while ((i = sis.read(cbuf, 0, 1024)) != -1) {
if (i == 1024) {
bb.put(cbuf);
} else {
byte[] tail = new byte[i];
System.arraycopy(cbuf, 0, tail, 0, i);
bb.put(tail);
}
}
byte[] data = new byte[bb.position()];
bb.position(0);
bb.get(data);
paramValue = new String(data, encode);
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
}
}
}
if (paramValue == null) { // parameter가 없는 경우때문에 처리
paramValue = "";
}
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] RECV (" + adptGrpName + ") = [" + paramValue + "]\n"
+ CommonLib.getDumpMessage(paramValue));
}
if ("Y".equals(urlDecodeYn) && isParameterType) {
message = URLDecoder.decode(paramValue);
} else {
message = paramValue;
}
if (logger.isDebug()) {
String[] msgArgs = new String[2];
msgArgs[0] = adptGrpName;
msgArgs[1] = message;
String resMsg = ExceptionUtil.make("RICEAIAHA005", msgArgs);
logger.debug(logPrefix + resMsg);
}
adptMsgType = adptVO.getAdapterGroupVO().getMessageType();
if (StringUtils.equals(adptMsgType, MessageType.JSON)) {
response.setContentType(JSON_CONTENT_TYPE+"; charset="+encode);
}
// HEADER_GROUP 셋팅
if (MessageType.JSON.equals(adptMsgType) && StringUtils.isNotBlank(headerGroupName)
&& StringUtils.isNotBlank(relayRequestHeaderKeys)) {
JSONObject jsonMessage = (JSONObject) JSONValue.parse(message);
JSONObject headerJson = new JSONObject();
if (StringUtils.equalsIgnoreCase(relayRequestHeaderKeys, "ALL")) {
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements();) {
String key = e.nextElement();
headerJson.put(key, request.getHeader(key));
}
} else {
String[] relayKeyArr = org.springframework.util.StringUtils
.tokenizeToStringArray(relayRequestHeaderKeys, ",");
for (String key : relayKeyArr) {
String headerValue = request.getHeader(key);
if (StringUtils.isNotBlank(headerValue)) {
headerJson.put(key, headerValue);
}
}
}
if (headerJson.size() > 0) {
jsonMessage.put(headerGroupName, headerJson);
message = jsonMessage.toJSONString();
}
}
if (message == null) {
message = "";
}
// com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
prop = addCryptoFilter(prop);
// 로컬 서비스 호출 ,encoding 처리 추가
String result = (String) service(adptGrpName, adptName, message, prop, request, response);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] result " + encode + " (" + adptGrpName + ") = [" + result + "]");
}
stopWatch.stop();
String responseData = "";
if (RESPONSE_TYPE_ASYNC.equals(responseType)) {
if (stopWatch.getTime() > slowTranTime && (logger.isInfo())) {
logger.info("HttpAdapterServiceRest] dummy response time = " + stopWatch.toString() + ", message = "
+ message);
}
if (result == null) {
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
}
response.setCharacterEncoding(encode);
response.getWriter().print(responseData);
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
}
} else {
responseData = result;
logger.info("종료 >> encode = [" + encode + "]");
JSONObject dataObject = null;
if (MessageType.JSON.equals(adptMsgType)) {
dataObject = (JSONObject) JSONValue.parse(responseData);
}
// HEADER_GROUP 하위 필드를 response Header에 세팅한다.
HashMap<String, String> header = new HashMap<>();
boolean redirect = assignHttpHeaders(header, dataObject, headerGroupName);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] response header field (" + adptGrpName + ") = ["
+ header.toString() + "]");
}
if (redirect) {
response.setStatus(302);
logger.debug("HttpAdapterServiceRest] set response status_code: 302");
} else {
String httpStatus = header.get(HTTP_STATUS);
if (httpStatus != null && !"".equals(httpStatus))
response.setStatus(Integer.parseInt(httpStatus));
header.remove(HTTP_STATUS);
}
// response header 셋팅
for (Map.Entry<String, String> entry : header.entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
if (dataObject != null) {
responseData = dataObject.toJSONString();
}
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
response.setCharacterEncoding(encode);
response.getWriter().print(responseData);
if (logger.isDebug()) {
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = [" + responseData + "]");
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = "
+ CommonLib.getDumpMessage(responseData));
}
if (traceLevel >= 3) {
HttpMemoryLogger.txlog(adptGrpName + adptName,
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
}
}
} catch (HttpStatusException e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.warn("HttpAdapter] " + adptGrpName + "-" + adptName + ">>" + e.getMessage());
response.setStatus(e.getStatus());
try {
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
e.getMessage(), errorResponseFormat);
response.getWriter().println(errorMsg);
} catch (Exception ex) {
// IGNORE
}
} catch (JwtAuthException e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
e.getMessage(), errorResponseFormat);
response.getWriter().println(errorMsg);
} catch (Exception ex) {
// IGNORE
}
} catch (Exception e) {
if (traceLevel >= 3) {
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
}
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
try {
response.getWriter().println(e.getMessage());
String errCode = ExceptionUtil.getErrorCode(e, "RECEAIAHA003");
throw new Exception(errCode);
} catch (Exception ex) {
// IGNORE
logger.warn(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
}
} finally {
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
String url = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_EXTURI);
String method = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_METHOD);
String adapterGroupName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
String adapterName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME);
int httpStatusCode = response.getStatus();
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, 400, adapterGroupName, adapterName, new HashMap<>(), url, method, httpStatusCode);
}
}
private Map<String, String[]> assignParameterMap(HttpServletRequest request, String adptGrpName, String adptName,
Object requestBytes, Properties prop) {
// PathVariable 체크
if (StringUtils.equalsAnyIgnoreCase(request.getMethod(), HttpMethod.GET.name(), HttpMethod.DELETE.name())
&& StringUtils.isBlank(request.getQueryString())) {
try {
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
RequestAction action = ActionFactory.createAction(actionName);
action.setAdapterInfo(adptGrpName, adptName, prop);
String[] keys = action.perform(requestBytes);
String requestPath = keys[0];
// PathVariable 지원 추가
String ruledPath = StandardMessageUtil.getMatchedKey(requestPath, actionName);
if (!StringUtils.equals(requestPath, ruledPath) && StringUtils.contains(ruledPath, "{")) {
Map<String, String> paramMap = new AntPathMatcher().extractUriTemplateVariables(ruledPath,
requestPath);
if (paramMap != null && paramMap.size() > 0) {
Map<String, String[]> returnMap = new HashMap<>();
for (String key : paramMap.keySet()) {
if (StringUtils.equalsIgnoreCase(key, "method")) {
continue;
}
returnMap.put(key, new String[] { paramMap.get(key) });
}
return returnMap;
}
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
return request.getParameterMap();
}
private void validateServiceAndAdapter(String adptGrpName, String adptName, byte[] requestBytes, Properties prop)
throws JwtAuthException {
}
/**
* request header 를 hashmap으로 조립
*
* @param request
* @return
*/
private Properties getHeaders(HttpServletRequest request) {
Properties prop = new Properties();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
prop.setProperty(key, value);
}
return prop;
}
/**
* adapter property의 HEADER_GROUP으로 정의된 (MFE_HEADER) 그룹의 하위 필드를 http Header에
* 세팅한다.
*
* @param header
* @param object
*/
private boolean assignHttpHeaders(HashMap<String, String> header, Object msg, String headerGroupName) {
boolean redirect = false;
if (msg == null || StringUtils.isBlank(headerGroupName)) {
return false;
}
if (msg instanceof JSONObject) {
JSONObject headerObject = (JSONObject) ((JSONObject) msg).get(headerGroupName);
if (headerObject == null) {
return false;
}
// List<String> headerKeys = new ArrayList<>();
for (Object key : headerObject.keySet()) {
Object obj = headerObject.get(key);
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
continue;
}
header.put((String) key, (String) obj);
if (StringUtils.equalsIgnoreCase((String) key, "Location")) {
redirect = true;
}
}
((JSONObject) msg).remove(headerGroupName);
}
return redirect;
}
/**
* 어댑터 명 이후의 URI값을 가져온다.
*
* @param request
* @return
*/
private String getExtUri(HttpServletRequest request) {
String orgUri = request.getRequestURI().replaceAll(request.getContextPath(), "");
String uri = getExtUri(orgUri, 3);
if (uri != null && uri.trim().length() > 0) {
return "/" + uri;
} else {
return "";
}
}
private static String getExtUri(String url, int length) {
String[] urls = url.split("/");
List<String> newUrls = new ArrayList<>();
Collections.addAll(newUrls, urls);
return StringUtils.join(newUrls.subList(length, urls.length).toArray(), "/");
}
// public static void main(String[] args) throws Exception {
// String orgUri = "/HTT/CbsInNetSys/abcd/123456";
// String result = "";
// result = getExtUri(orgUri, 3);
// System.out.println(result);
// }
}
@@ -163,7 +163,14 @@ public class EncryptionManager implements Lifecycle {
if (this.isEncrypted(dbData)) {
if ("DAMO".equals(dbEncryptSolutionName)) {
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
return damoManager.decrypt(dbData);
try {
return damoManager.decrypt(dbData);
} catch (Exception e) {
// 복호화 실패시 원본 반환
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
return dbData;
}
} else if ("SAFEDB".equals(dbEncryptSolutionName)) {
KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
try {
@@ -2,10 +2,8 @@ package com.eactive.eai.common.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
@@ -63,13 +61,17 @@ public class HttpAdapterExtraLogUtil {
httpAdapterExtraLogVo.setHttpMethod(httpMethod);
for (HttpAdapterExtraHeaderVo httpAdapterExtraHeaderVo : headerVoList) {
// String name = httpAdapterExtraHeaderVo.getName();
String name = httpAdapterExtraHeaderVo.getName();
// if(StringUtils.isNotBlank(name) && "authorization".equals(name.toLowerCase())) {
// httpAdapterExtraHeaderVo.setValue("{hidden}");
// }
if(StringUtils.isNotBlank(name) && BODY_FIELD_NAME.equals(name))
continue;
String value = httpAdapterExtraHeaderVo.getValue();
if(StringUtils.isNotBlank(value) && value.length() > MAX_HEADER_VALUE_SIZE) {
value = value.substring(0, 400) + "...";
value = value.substring(0, MAX_HEADER_VALUE_SIZE) + "...";
httpAdapterExtraHeaderVo.setValue(value);
}else if(value == null){
httpAdapterExtraHeaderVo.setValue(" ");
@@ -116,7 +118,7 @@ public class HttpAdapterExtraLogUtil {
}
public static List<HttpAdapterExtraHeaderVo> convertHeaderToListOfHttpAdapterExtraHeaderVo(Header[] headers) {
Set<String> seenNames = new HashSet<>();
// Set<String> seenNames = new HashSet<>();
return Arrays.stream(headers)
// .filter(header -> seenNames.add(header.getName())) // 중복된 이름을 스킵
.map(header -> new HttpAdapterExtraHeaderVo(header.getName(), header.getValue()))
@@ -123,7 +123,7 @@ public class RequestProcessor extends RequestProcessorSupport {
// UUID 생성 : UUID에서 - 없는 32자리
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
uuid = uuid == null ? UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
uuid = uuid == null ? instid+UUIDGenerator.getUUID().toString().replaceAll("-", "") : uuid;
// UUID 생성 : UUID = server구분4자리 + UUID
/*
String uuid = "";
@@ -0,0 +1,15 @@
package com.eactive.eai.manage.tools;
import lombok.Data;
/**
* EncryptionManager의 현재 설정 상태. encrypt/decrypt 결과가 원문 그대로인 이유
* (encryptYN=N이면 encryptDBData가 원문을 그대로 반환)를 함께 확인하기 위한 진단 정보다.
*/
@Data
public class EncryptionManagerStatusDTO {
String encryptYN;
String dbEncryptSolutionName;
boolean encryptEnabled;
}
@@ -0,0 +1,123 @@
package com.eactive.eai.manage.tools;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* base64 / hex / DAMO 변환을 브라우저나 Postman 등에서 바로 테스트할 수 있는 유틸리티 API.
*
* POST /manage/tools/base64/encode → 문자열 → Base64
* POST /manage/tools/base64/decode → Base64 → 문자열
* POST /manage/tools/hex/encode → 문자열 → Hex
* POST /manage/tools/hex/decode → Hex → 문자열
* POST /manage/tools/damo/encrypt → 평문 → DAMO 암호문 (DamoManager 직접 호출)
* POST /manage/tools/damo/decrypt → DAMO 암호문 → 평문 (DamoManager 직접 호출)
*
* POST /manage/tools/encryption-manager/encrypt → EncryptionManager.encryptDBData 확인용
* POST /manage/tools/encryption-manager/decrypt → EncryptionManager.decryptDBData 확인용
* GET /manage/tools/encryption-manager/status → encryptYN/dbEncryptSolutionName 등 현재 설정 확인
*
* GET /manage/tools/version → 배포된 게이트웨이(eapim-online.war)의 git 버전/빌드시각 확인.
* eapim-online(WAR를 만드는 루트 프로젝트) build.gradle의
* generateVersionInfo 태스크가 생성하는 classpath 리소스
* version.info(git describe 결과) 기반. 재빌드 없이 기동한 로컬
* 환경 등 파일이 없으면 success=false로 응답한다
*
* base64/hex 변환은 요청 body의 charset(생략 시 UTF-8)을 기준으로 문자열 ↔ 바이트를 변환한다.
*/
@RestController
@RequestMapping("/manage/tools")
public class ToolsController {
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
@Autowired
private ToolsService toolsService;
@PostMapping("/base64/encode")
public ResponseEntity<?> base64Encode(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.base64Encode(request.getText(), request.getCharset()));
}
@PostMapping("/base64/decode")
public ResponseEntity<?> base64Decode(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.base64Decode(request.getText(), request.getCharset()));
}
@PostMapping("/hex/encode")
public ResponseEntity<?> hexEncode(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.hexEncode(request.getText(), request.getCharset()));
}
@PostMapping("/hex/decode")
public ResponseEntity<?> hexDecode(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.hexDecode(request.getText(), request.getCharset()));
}
@PostMapping("/damo/encrypt")
public ResponseEntity<?> damoEncrypt(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.damoEncrypt(request.getText()));
}
@PostMapping("/damo/decrypt")
public ResponseEntity<?> damoDecrypt(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.damoDecrypt(request.getText()));
}
@PostMapping("/encryption-manager/encrypt")
public ResponseEntity<?> encryptionManagerEncrypt(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.encryptionManagerEncrypt(request.getText()));
}
@PostMapping("/encryption-manager/decrypt")
public ResponseEntity<?> encryptionManagerDecrypt(@RequestBody ToolsTextRequestDTO request) {
return respond(() -> toolsService.encryptionManagerDecrypt(request.getText()));
}
@GetMapping("/encryption-manager/status")
public ResponseEntity<?> encryptionManagerStatus() {
return respond(() -> toolsService.encryptionManagerStatus());
}
@GetMapping("/version")
public ResponseEntity<?> getVersionInfo() {
Map<String, Object> result = new HashMap<>();
try {
VersionInfoDTO versionInfo = toolsService.getVersionInfo();
if (versionInfo == null) {
result.put("success", false);
result.put("message", "version.info 파일이 없습니다. gradle의 generateVersionInfo 태스크(또는 build) 실행 후 재기동하세요.");
} else {
result.put("success", true);
result.put("data", versionInfo);
}
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok().contentType(APPLICATION_JSON_UTF8).body(result);
}
private ResponseEntity<?> respond(Callable<Object> action) {
Map<String, Object> result = new HashMap<>();
try {
result.put("success", true);
result.put("data", action.call());
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok().contentType(APPLICATION_JSON_UTF8).body(result);
}
}
@@ -0,0 +1,126 @@
package com.eactive.eai.manage.tools;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.eactive.eai.agent.encryption.EncryptionManager;
/**
* base64 / hex / DAMO 변환을 테스트하기 위한 서비스.
*
* DAMO 암복호화는 EncryptionManager와 동일하게 com.eactive.ext.djb.DamoManager를 직접 사용한다.
* EncryptionManager.encryptDBData/decryptDBData는 encryptYN=Y로 설정된 환경에서만 동작하므로
* (비활성 환경에서는 원문을 그대로 반환), 환경 설정과 무관하게 항상 실제 암복호화를 확인할 수 있도록
* DamoManager를 직접 호출한다.
*
* encryptionManagerEncrypt/Decrypt는 위와 별개로, 실제 운영 코드가 사용하는
* EncryptionManager.encryptDBData/decryptDBData를 그대로 호출해 현재 서버 설정(encryptYN,
* dbEncryptSolutionName)이 반영된 실제 동작을 확인하기 위한 용도다.
*/
@Service
public class ToolsService {
private static final String MODULE_KEY_PREFIX = "module.";
private Charset resolveCharset(String charset) {
return StringUtils.isNotBlank(charset) ? Charset.forName(charset) : StandardCharsets.UTF_8;
}
public String base64Encode(String text, String charset) {
byte[] bytes = text.getBytes(resolveCharset(charset));
return Base64.getEncoder().encodeToString(bytes);
}
public String base64Decode(String base64Text, String charset) {
byte[] bytes = Base64.getDecoder().decode(base64Text);
return new String(bytes, resolveCharset(charset));
}
public String hexEncode(String text, String charset) {
byte[] bytes = text.getBytes(resolveCharset(charset));
return DatatypeConverter.printHexBinary(bytes);
}
public String hexDecode(String hexText, String charset) {
byte[] bytes = DatatypeConverter.parseHexBinary(hexText.trim());
return new String(bytes, resolveCharset(charset));
}
public String damoEncrypt(String text) {
return new com.eactive.ext.djb.DamoManager().encrypt(text);
}
public String damoDecrypt(String text) {
return new com.eactive.ext.djb.DamoManager().decrypt(text);
}
public String encryptionManagerEncrypt(String text) {
return EncryptionManager.getInstance().encryptDBData(text);
}
public String encryptionManagerDecrypt(String text) {
return EncryptionManager.getInstance().decryptDBData(text);
}
public EncryptionManagerStatusDTO encryptionManagerStatus() {
EncryptionManager manager = EncryptionManager.getInstance();
EncryptionManagerStatusDTO status = new EncryptionManagerStatusDTO();
status.setEncryptYN(manager.getEncryptYN());
status.setDbEncryptSolutionName(manager.getDBEncryptSolutionName());
status.setEncryptEnabled(manager.isEncrypt());
return status;
}
/**
* eapim-online(WAR를 생성하는 루트 프로젝트) build.gradle의 generateVersionInfo 태스크가
* processResources 이전에 생성하는 classpath 리소스 version.info(version=git describe 결과,
* buildTime=...)를 읽는다. WAR는 WEB-INF/classes와 WEB-INF/lib가 클래스로더를 공유하므로
* 이 클래스(elink-online-common 모듈)에서 조회해도 정상적으로 찾을 수 있다.
* 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등 파일이 없는 경우 null을 반환한다.
*/
public VersionInfoDTO getVersionInfo() {
try (InputStream in = getClass().getResourceAsStream("/version.info")) {
if (in == null) {
return null;
}
return parseVersionInfo(in);
} catch (IOException e) {
throw new RuntimeException("version.info 읽기 실패", e);
}
}
/** version.info 내용을 파싱한다. 리소스 조회와 분리해 테스트에서 직접 검증할 수 있게 한다. */
VersionInfoDTO parseVersionInfo(InputStream in) throws IOException {
Properties props = new Properties();
// Properties.load(InputStream)은 ISO-8859-1로 고정 해석되어 UTF-8로 기록된
// version.info의 한글(git tag 등)이 깨진다. Reader로 감싸 UTF-8로 디코딩해서 넘긴다.
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
VersionInfoDTO dto = new VersionInfoDTO();
dto.setVersion(props.getProperty("version"));
dto.setBuildTime(props.getProperty("buildTime"));
// module.<서브모듈명>=<git describe 결과> 형태의 키를 모아 서브모듈별 버전으로 담는다.
Map<String, String> moduleVersions = new TreeMap<>();
for (String key : props.stringPropertyNames()) {
if (key.startsWith(MODULE_KEY_PREFIX)) {
moduleVersions.put(key.substring(MODULE_KEY_PREFIX.length()), props.getProperty(key));
}
}
dto.setModuleVersions(moduleVersions);
return dto;
}
}
@@ -0,0 +1,16 @@
package com.eactive.eai.manage.tools;
import lombok.Data;
/**
* base64/hex/damo 변환 테스트 요청 DTO.
*
* charset은 base64/hex 변환에서 문자열 ↔ 바이트 변환에 사용되며, 생략 시 UTF-8이 적용된다.
* damo 암복호화는 String 기반 API(DamoManager)를 그대로 사용하므로 charset을 사용하지 않는다.
*/
@Data
public class ToolsTextRequestDTO {
String text;
String charset;
}
@@ -0,0 +1,25 @@
package com.eactive.eai.manage.tools;
import java.util.Map;
import lombok.Data;
/**
* 빌드 시점에 생성되는 version.info(classpath 리소스) 내용을 담는 DTO.
* eapim-online(WAR 루트 프로젝트) build.gradle의 generateVersionInfo 태스크가
* processResources 이전에 생성한다.
*/
@Data
public class VersionInfoDTO {
/** git describe --tags --always --dirty 결과 (예: 20260722_개발배포, 또는 태그 없으면 커밋 해시). */
String version;
String buildTime;
/**
* elink-online-common 등 git submodule 각각의 git describe 결과.
* 키는 submodule 디렉토리명(elink-online-common 등), 값은 describe 결과.
* 루트(eapim-online)의 describe/dirty만으로는 어느 서브모듈이 바뀌었는지 알 수 없어서 별도로 담는다.
*/
Map<String, String> moduleVersions;
}
@@ -704,6 +704,18 @@ public abstract class DefaultProcess extends Process {
Object responseObj = adapterErrorMessageHandler.generateNonStandardErrorResponseMessage(inboudnAdapterGroupName, inboudnAdapterName, this.callProp, this.tgtTranObject, this.resEaiMsg);
if(responseObj != null) {
resStandardMessage.setBizData(responseObj, inboundAdapterGroupVO.getMessageEncode());
} else {
if ((com.eactive.eai.adapter.Keys.IF_STANDARD.equals(this.adptrMsgPtrnCd)
||com.eactive.eai.adapter.Keys.IF_SUBSTANDARD.equals(this.adptrMsgPtrnCd))) {
String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc"));
this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc));
return;
} else {
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신");
return;
}
}
this.resEaiMsg.setRspErrCd(EAIMessageKeys.BWK_FAILMSG_CODE, false);
@@ -0,0 +1,108 @@
package com.eactive.eai.util;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.json.simple.JSONValue;
public class QueryStringUtils {
private QueryStringUtils() {
throw new IllegalStateException("Utility class");
}
public static Map<String, String[]> parseQueryString(String queryString, String charset) throws UnsupportedEncodingException {
return parseQueryString(queryString, charset, true);
}
public static Map<String, String[]> parseQueryString(String queryString) throws UnsupportedEncodingException {
return parseQueryString(queryString, null, false);
}
public static Map<String, String[]> parseQueryString(String queryString, String charset, boolean urlDecode) throws UnsupportedEncodingException {
Map<String, String[]> paramMap = new HashMap<>();
Map<String, List<String>> tempMap = new HashMap<>();
if(StringUtils.isEmpty(queryString))
return paramMap;
String[] pairs = queryString.split("&");
for(String pair : pairs) {
if(StringUtils.isEmpty(pair))
continue;
int idx = pair.indexOf("=");
String key = null;
String value = null;
if(idx > 0) {
if(urlDecode)
key = URLDecoder.decode(pair.substring(0, idx), charset);
else
if(charset != null)
key = new String((pair.substring(0, idx)).getBytes(charset));
else
key = pair.substring(0, idx);
if(urlDecode)
value = URLDecoder.decode(pair.substring(idx + 1), charset);
else
if(charset != null)
value = new String((pair.substring(idx + 1)).getBytes(charset));
else
value = pair.substring(idx + 1);
} else {
key = urlDecode ? URLDecoder.decode(pair, charset) : new String(pair.getBytes(charset));
value = "";
}
tempMap.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
for(Map.Entry<String, List<String>> entry : tempMap.entrySet()) {
paramMap.put(entry.getKey(), entry.getValue().toArray(new String[0]));
}
return paramMap;
}
public static String makeJson(Map<String, String[]> paramMap, boolean bUrlDecode) throws UnsupportedEncodingException {
StringBuilder sb = new StringBuilder();
sb.append("{");
int i = 0;
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
if (i > 0) {
sb.append(",");
}
sb.append("\"").append(entry.getKey()).append("\":");
String[] values = entry.getValue();
if (values.length > 1) {
// ["111", "222"]
sb.append("[");
for (int j = 0; j < values.length; j++) {
if (j > 0) {
sb.append(",");
}
String value = bUrlDecode ? URLDecoder.decode(values[j], StandardCharsets.UTF_8.name()) : values[j];
sb.append("\"").append(JSONValue.escape(value)).append("\"");
}
sb.append("]");
} else {
String value = bUrlDecode ? URLDecoder.decode(values[0], StandardCharsets.UTF_8.name()) : values[0];
sb.append("\"").append(JSONValue.escape(value)).append("\"");
}
i++;
}
sb.append("}");
return sb.toString();
}
}
@@ -1383,4 +1383,428 @@ class TemplateAdapterErrorMsgHandlerTest {
assertNotNull(result);
}
}
// ================================================================
// default.* 템플릿 폴백
//
// 그룹 전용 키가 미설정/공백이면 "default." 로 시작하는 공통 키를 재조회한다.
// generateNonStandardErrorResponseMessage : {group}.template → default.template
// generateOutboundErrorResponseMessage : {group}.template → default.template
// generateNonStandardInternalErrorResponseMessage : {group}.sys.template → default.sys.template
// generateNonStandardInboundErrorResponseMessage : {group}.in.{code}.template
// → default.in.{code}.template
// → {group}.in.template
// → default.in.template
// → MessageUtil 폴백
// ================================================================
@Nested
@DisplayName("default.* 템플릿 폴백")
class DefaultTemplateFallback {
private static final String G = TemplateAdapterErrorMsgHandler.PROP_GROUP;
private PropManager mockPropManager;
private AdapterManager mockAdapterManager;
private AdapterPropManager mockAdapterPropManager;
@BeforeEach
void setUpDefaults() throws Exception {
mockPropManager = Mockito.mock(PropManager.class);
mockAdapterManager = Mockito.mock(AdapterManager.class);
mockAdapterPropManager = Mockito.mock(AdapterPropManager.class);
// 명시적으로 stub 하지 않은 모든 키는 미설정(null) 로 간주
Mockito.when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null);
Mockito.when(mockPropManager.getProperty(anyString(), anyString(), anyString()))
.thenAnswer(invocation -> invocation.getArgument(2));
AdapterGroupVO adapterGroupVO = Mockito.mock(AdapterGroupVO.class);
AdapterVO adapterVO = Mockito.mock(AdapterVO.class);
Mockito.when(adapterGroupVO.getMessageType()).thenReturn("JSON");
Mockito.when(adapterGroupVO.getMessageEncode()).thenReturn("UTF-8");
Mockito.when(adapterVO.getPropGroupName()).thenReturn("TEST_PROP");
Mockito.when(adapterVO.getAdapterGroupVO()).thenReturn(adapterGroupVO);
Mockito.when(mockAdapterManager.getAdapterGroupVO("MY_GROUP")).thenReturn(adapterGroupVO);
Mockito.when(mockAdapterManager.getAdapterVO("MY_GROUP", "MY_ADAPTER")).thenReturn(adapterVO);
Properties httpProp = new Properties();
httpProp.setProperty("ERROR_RESPONSE_FORMAT", "");
Mockito.when(mockAdapterPropManager.getProperties("TEST_PROP")).thenReturn(httpProp);
ApplicationContext mockCtx = Mockito.mock(ApplicationContext.class);
Mockito.when(mockCtx.getBean(PropManager.class)).thenReturn(mockPropManager);
Mockito.when(mockCtx.getBean(AdapterManager.class)).thenReturn(mockAdapterManager);
Mockito.when(mockCtx.getBean(AdapterPropManager.class)).thenReturn(mockAdapterPropManager);
Field ctxField = ApplicationContextProvider.class.getDeclaredField("context");
ctxField.setAccessible(true);
ctxField.set(null, mockCtx);
}
private void prop(String key, String value) {
Mockito.when(mockPropManager.getProperty(eq(G), eq(key))).thenReturn(value);
}
// ------------------------------------------------------------
// generateNonStandardErrorResponseMessage : default.template
// ------------------------------------------------------------
@Nested
@DisplayName("generateNonStandardErrorResponseMessage default.template")
class NonStandardError {
@Test
@DisplayName("그룹 템플릿이 있으면 default.template 을 조회하지 않는다")
void 그룹_템플릿_우선() throws Exception {
prop("MY_GROUP.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
assertEquals("{\"code\":\"E001\"}", result);
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
}
@Test
@DisplayName("그룹 템플릿 미설정 시 default.template 으로 폴백")
void default_폴백() throws Exception {
prop("default.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"src\":\"default\"}");
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E777", "오류", "", null)));
assertEquals("{\"code\":\"E777\",\"src\":\"default\"}", result);
}
@Test
@DisplayName("그룹 템플릿이 공백 문자열이어도 default.template 으로 폴백")
void 공백_템플릿도_폴백() throws Exception {
prop("MY_GROUP.template", " ");
prop("default.template", "{\"src\":\"default\"}");
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
assertEquals("{\"src\":\"default\"}", result);
}
@Test
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
void 둘다_없으면_null() throws Exception {
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
assertNull(result);
// 폴백 조회가 실제로 시도되었는지 확인
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.template");
Mockito.verify(mockPropManager).getProperty(G, "default.template");
}
@Test
@DisplayName("조회 순서: {group}.template → default.template")
void 조회_순서() throws Exception {
handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
InOrder inOrder = Mockito.inOrder(mockPropManager);
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.template");
inOrder.verify(mockPropManager).getProperty(G, "default.template");
}
@Test
@DisplayName("default.template 에서도 callProp/foreach 치환이 동일하게 동작")
void default_템플릿_전체_치환() throws Exception {
prop("default.template",
"{\"adapter\":\"${callprop.ADAPTER_NAME}\"," +
"\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"," +
"\"errors\":[{{#foreach MSG.MSG_LIST}}{\"c\":\"${outp_msg_cd}\"}{{/foreach}}]}");
Object result = handler.generateNonStandardErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", props("ADAPTER_NAME", "REAL"), null,
toEaiMessage(buildMsg("E001", "대표오류", "",
new String[][]{{"E001", "오류A", "", ""}, {"E002", "오류B", "", ""}})));
assertEquals(
"{\"adapter\":\"REAL\",\"code\":\"E001\"," +
"\"errors\":[{\"c\":\"E001\"},{\"c\":\"E002\"}]}",
result);
}
}
// ------------------------------------------------------------
// generateNonStandardInternalErrorResponseMessage : default.sys.template
// ------------------------------------------------------------
@Nested
@DisplayName("generateNonStandardInternalErrorResponseMessage default.sys.template")
class InternalError {
@Test
@DisplayName("그룹 sys 템플릿이 있으면 default.sys.template 을 조회하지 않는다")
void 그룹_템플릿_우선() throws Exception {
prop("MY_GROUP.sys.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E500", "내부오류", "", null)));
assertEquals("{\"code\":\"E500\"}", result);
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.sys.template");
}
@Test
@DisplayName("그룹 sys 템플릿 미설정 시 default.sys.template 으로 폴백")
void default_폴백() throws Exception {
prop("default.sys.template",
"{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"msg\":\"${MSG.MAIN_MSG.outp_msg_ctnt}\"}");
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E500", "시스템오류", "", null)));
assertEquals("{\"code\":\"E500\",\"msg\":\"시스템오류\"}", result);
}
@Test
@DisplayName("내부오류는 default.template 이 아니라 default.sys.template 을 조회한다")
void sys_전용_키_사용() throws Exception {
// default.template 만 설정된 상태 → 내부오류에는 적용되지 않아야 한다
prop("default.template", "{\"src\":\"non-sys\"}");
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E500", "시스템오류", "", null)));
assertNull(result);
Mockito.verify(mockPropManager).getProperty(G, "default.sys.template");
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
}
@Test
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
void 둘다_없으면_null() throws Exception {
Object result = handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E500", "오류", "", null)));
assertNull(result);
}
@Test
@DisplayName("조회 순서: {group}.sys.template → default.sys.template")
void 조회_순서() throws Exception {
handler.generateNonStandardInternalErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null,
toEaiMessage(buildMsg("E500", "오류", "", null)));
InOrder inOrder = Mockito.inOrder(mockPropManager);
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.sys.template");
inOrder.verify(mockPropManager).getProperty(G, "default.sys.template");
}
}
// ------------------------------------------------------------
// generateOutboundErrorResponseMessage : default.template
// ------------------------------------------------------------
@Nested
@DisplayName("generateOutboundErrorResponseMessage default.template")
class OutboundError {
@Test
@DisplayName("그룹 템플릿이 있으면 default.template 을 조회하지 않는다")
void 그룹_템플릿_우선() throws Exception {
prop("OUT_GROUP.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
Object result = handler.generateOutboundErrorResponseMessage(
"OUT_GROUP", "OUT_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
assertEquals("{\"code\":\"E001\"}", result);
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.template");
}
@Test
@DisplayName("그룹 템플릿 미설정 시 default.template 으로 폴백")
void default_폴백() throws Exception {
prop("default.template", "{\"code\":\"${MSG.MAIN_MSG.outp_msg_cd}\",\"src\":\"default\"}");
Object result = handler.generateOutboundErrorResponseMessage(
"OUT_GROUP", "OUT_ADAPTER", null, null,
toEaiMessage(buildMsg("E888", "오류", "", null)));
assertEquals("{\"code\":\"E888\",\"src\":\"default\"}", result);
}
@Test
@DisplayName("아웃바운드/인바운드가 동일한 default.template 을 공유한다")
void inbound_outbound_default_공유() throws Exception {
prop("default.template", "{\"shared\":\"${MSG.MAIN_MSG.outp_msg_cd}\"}");
Object outbound = handler.generateOutboundErrorResponseMessage(
"OUT_GROUP", "OUT_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
Object inbound = handler.generateNonStandardErrorResponseMessage(
"IN_GROUP", "IN_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
// 두 경로 모두 같은 키를 쓰므로 결과가 동일하다.
// 방향별로 다른 기본 응답이 필요하면 그룹 전용 키를 설정해야 한다.
assertEquals("{\"shared\":\"E001\"}", outbound);
assertEquals(outbound, inbound);
}
@Test
@DisplayName("그룹/default 둘 다 없으면 기존대로 null 반환")
void 둘다_없으면_null() throws Exception {
Object result = handler.generateOutboundErrorResponseMessage(
"OUT_GROUP", "OUT_ADAPTER", null, null,
toEaiMessage(buildMsg("E001", "오류", "", null)));
assertNull(result);
}
}
// ------------------------------------------------------------
// generateNonStandardInboundErrorResponseMessage : default.in.*
// ------------------------------------------------------------
@Nested
@DisplayName("generateNonStandardInboundErrorResponseMessage default.in.*")
class InboundError {
/** 500 으로 매핑되는 일반 예외로 호출한다. */
private Object call500(Properties callProp) throws Exception {
return handler.generateNonStandardInboundErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", callProp, null, null,
new RuntimeException("처리 중 오류"));
}
@Test
@DisplayName("1순위: {group}.in.{code}.template 이 있으면 그것을 사용")
void 그룹_코드별_템플릿_우선() throws Exception {
prop("MY_GROUP.in.500.template", "{\"src\":\"group-code\"}");
prop("default.in.500.template", "{\"src\":\"default-code\"}");
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
prop("default.in.template", "{\"src\":\"default-common\"}");
assertEquals("{\"src\":\"group-code\"}", call500(null));
}
@Test
@DisplayName("2순위: {group}.in.{code} 가 없으면 default.in.{code}.template 사용")
void default_코드별_템플릿() throws Exception {
prop("default.in.500.template", "{\"src\":\"default-code\"}");
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
prop("default.in.template", "{\"src\":\"default-common\"}");
// 주의: 현재 구현은 코드별 default 가 그룹 공통보다 우선한다.
assertEquals("{\"src\":\"default-code\"}", call500(null));
}
@Test
@DisplayName("3순위: 코드별 키가 모두 없으면 {group}.in.template 사용")
void 그룹_공통_템플릿() throws Exception {
prop("MY_GROUP.in.template", "{\"src\":\"group-common\"}");
prop("default.in.template", "{\"src\":\"default-common\"}");
assertEquals("{\"src\":\"group-common\"}", call500(null));
}
@Test
@DisplayName("4순위: 앞의 3개가 모두 없으면 default.in.template 사용")
void default_공통_템플릿() throws Exception {
prop("default.in.template", "{\"src\":\"default-common\"}");
assertEquals("{\"src\":\"default-common\"}", call500(null));
}
@Test
@DisplayName("4개 키가 모두 없으면 MessageUtil 폴백")
void 전부_없으면_MessageUtil_폴백() throws Exception {
Object result = call500(null);
assertNotNull(result);
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.in.500.template");
Mockito.verify(mockPropManager).getProperty(G, "default.in.500.template");
Mockito.verify(mockPropManager).getProperty(G, "MY_GROUP.in.template");
Mockito.verify(mockPropManager).getProperty(G, "default.in.template");
}
@Test
@DisplayName("조회 순서: group.code → default.code → group.common → default.common")
void 조회_순서_4단계() throws Exception {
handler.generateNonStandardInboundErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null, null,
new HttpStatusException("Service Unavailable", 503));
InOrder inOrder = Mockito.inOrder(mockPropManager);
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.in.503.template");
inOrder.verify(mockPropManager).getProperty(G, "default.in.503.template");
inOrder.verify(mockPropManager).getProperty(G, "MY_GROUP.in.template");
inOrder.verify(mockPropManager).getProperty(G, "default.in.template");
}
@Test
@DisplayName("코드별 default 키 형식은 'default.in.{code}.template' (점 누락 회귀 방지)")
void default_코드별_키_형식_검증() throws Exception {
handler.generateNonStandardInboundErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null, null,
new HttpStatusException("Bad Request", 400));
Mockito.verify(mockPropManager).getProperty(G, "default.in.400.template");
// 점이 빠진 잘못된 키로 조회하지 않아야 한다
Mockito.verify(mockPropManager, Mockito.never()).getProperty(G, "default.in400.template");
}
@Test
@DisplayName("JwtAuthException 은 default.in.401.template 로 폴백")
void jwt_401_default_폴백() throws Exception {
prop("default.in.401.template", "{\"code\":\"${exception.code}\",\"msg\":\"${exception.message}\"}");
Object result = handler.generateNonStandardInboundErrorResponseMessage(
"MY_GROUP", "MY_ADAPTER", null, null, null,
new JwtAuthException("JWT_EXPIRED", "토큰이 만료되었습니다"));
assertEquals("{\"code\":\"JWT_EXPIRED\",\"msg\":\"토큰이 만료되었습니다\"}", result);
}
@Test
@DisplayName("default.in.template 에서 exception/callProp 치환이 동일하게 동작")
void default_공통_템플릿_치환() throws Exception {
prop("default.in.template",
"{\"adapter\":\"${callprop.ADAPTER_NAME}\",\"msg\":\"${exception.message}\"}");
Object result = call500(props("ADAPTER_NAME", "REAL"));
assertEquals("{\"adapter\":\"REAL\",\"msg\":\"처리 중 오류\"}", result);
}
@Test
@DisplayName("default.in.template 에 ${callprop[경로]} 를 써도 msg=null 로 NPE 없이 기본값 반환")
void default_공통_템플릿_간접참조_NPE_회귀방지() throws Exception {
// 인바운드 경로는 render(template, null, ...) 로 호출되므로
// 간접 참조의 표준전문 경로 조회 대상이 없다 → 기본값으로 처리되어야 한다.
prop("default.in.template", "{\"key\":\"${callprop[MSG.cp_key]:NO_MSG}\"}");
Object result = call500(props("ADAPTER_NAME", "REAL"));
assertEquals("{\"key\":\"NO_MSG\"}", result);
}
@Test
@DisplayName("그룹 코드별 템플릿이 공백이면 default 로 폴백")
void 공백_템플릿도_폴백() throws Exception {
prop("MY_GROUP.in.500.template", " ");
prop("default.in.500.template", "{\"src\":\"default-code\"}");
assertEquals("{\"src\":\"default-code\"}", call500(null));
}
}
}
}
@@ -0,0 +1,319 @@
package com.eactive.eai.adapter.http.dynamic.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.json.simple.JSONObject;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.common.property.PropManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* JsonToSetStatusFilter 단위 테스트.
*
* 상태코드 필드명은 PropManager(그룹: JsonToSetStatusFilter, 키: 어댑터그룹명)에서 조회하므로
* PropManager 를 Mock 으로 등록하고 필터 로직만 검증한다.
*
* 4번 그룹은 "현재 구현의 동작을 그대로 고정(characterization)"한 테스트로,
* 개선 여부 판단용이다. 구현을 보완하면 해당 테스트도 함께 수정해야 한다.
*/
@TestMethodOrder(MethodOrderer.DisplayName.class)
class JsonToSetStatusFilterTest {
private static final String GRP = "TEST_GRP";
private static final String ADPT = "TEST_ADPT";
private static final String FIELD = "apiRsltCd";
private static final String PROP_GROUP = "JsonToSetStatusFilter";
private static GenericApplicationContext ctx;
private static PropManager mockPropManager;
private static JsonToSetStatusFilter filter;
private HttpServletRequest mockRequest;
private HttpServletResponse mockResponse;
private Properties prop;
@BeforeAll
static void setUpClass() {
mockPropManager = mock(PropManager.class);
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("propManager", mockPropManager);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
filter = new JsonToSetStatusFilter();
}
@AfterAll
static void tearDownClass() {
if (ctx != null) ctx.close();
}
@BeforeEach
void setUp() {
reset(mockPropManager);
mockRequest = mock(HttpServletRequest.class);
mockResponse = mock(HttpServletResponse.class);
prop = new Properties();
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(FIELD);
}
private String body(String rsltCd) {
return "{\"" + FIELD + "\":\"" + rsltCd + "\",\"msg\":\"OK\"}";
}
// =========================================================================
// 1. 상태코드 정상 반영
// =========================================================================
@Test
@DisplayName("1-1. 문자열 body의 상태코드 필드값을 HTTP 상태코드로 설정한다")
void testPostFilter_string_setsStatus() throws Exception {
Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(404);
assertEquals(body("404"), result, "원 메시지를 그대로 반환해야 한다");
}
@Test
@DisplayName("1-2. 숫자 타입 필드값도 상태코드로 설정한다")
void testPostFilter_numericNode_setsStatus() throws Exception {
filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":503}", prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(503);
}
@Test
@DisplayName("1-3. 업무팀 주 사용 케이스 - 2xx 상태코드를 설정한다")
void testPostFilter_successStatusCodes() throws Exception {
for (int status : new int[] { 200, 201, 202, 204 }) {
reset(mockResponse);
filter.doPostFilter(GRP, ADPT, body(String.valueOf(status)), prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(status);
}
}
@Test
@DisplayName("1-4. 어댑터 그룹마다 다른 필드명을 사용할 수 있다")
void testPostFilter_perGroupFieldName() throws Exception {
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn("rspCd");
filter.doPostFilter(GRP, ADPT, "{\"rspCd\":\"403\",\"" + FIELD + "\":\"500\"}", prop,
mockRequest, mockResponse);
verify(mockResponse).setStatus(403);
verify(mockResponse, never()).setStatus(500);
}
@Test
@DisplayName("1-5. JSONObject 타입 응답도 처리한다")
void testPostFilter_jsonObjectMessage_setsStatus() throws Exception {
JSONObject json = new JSONObject();
json.put(FIELD, "404");
Object result = filter.doPostFilter(GRP, ADPT, json, prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(404);
assertSame(json, result);
}
@Test
@DisplayName("1-6. JsonNode 타입 응답도 처리한다")
void testPostFilter_jsonNodeMessage_setsStatus() throws Exception {
JsonNode node = new ObjectMapper().readTree(body("401"));
Object result = filter.doPostFilter(GRP, ADPT, node, prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(401);
assertSame(node, result);
}
@Test
@DisplayName("1-7. 경계값 100 / 599는 설정한다")
void testPostFilter_boundaryValues() throws Exception {
filter.doPostFilter(GRP, ADPT, body("100"), prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(100);
reset(mockResponse);
filter.doPostFilter(GRP, ADPT, body("599"), prop, mockRequest, mockResponse);
verify(mockResponse).setStatus(599);
}
// =========================================================================
// 2. 상태코드를 변경하지 않는 경우
// =========================================================================
@Test
@DisplayName("2-1. 상태코드 필드가 없으면 setStatus를 호출하지 않는다")
void testPostFilter_fieldAbsent_noStatusChange() throws Exception {
filter.doPostFilter(GRP, ADPT, "{\"msg\":\"OK\"}", prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("2-2. 상태코드 필드값이 빈 문자열이면 setStatus를 호출하지 않는다")
void testPostFilter_emptyValue_noStatusChange() throws Exception {
filter.doPostFilter(GRP, ADPT, body(""), prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("2-3. 프로퍼티에 필드명 설정이 없으면(null) 예외 없이 통과한다")
void testPostFilter_noConfiguredField_noStatusChange() throws Exception {
when(mockPropManager.getProperty(PROP_GROUP, GRP)).thenReturn(null);
Object result = filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
assertEquals(body("404"), result);
}
@Test
@DisplayName("2-4. 상태코드 필드값이 숫자가 아니면 로그만 남기고 상태코드를 변경하지 않는다")
void testPostFilter_nonNumericValue_noStatusChange() throws Exception {
Object result = filter.doPostFilter(GRP, ADPT, body("E0001"), prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
assertEquals(body("E0001"), result, "파싱 실패해도 원 메시지는 그대로 반환되어야 한다");
}
@Test
@DisplayName("2-5. 업무결과코드 0000은 유효한 상태코드가 아니므로 설정하지 않는다")
void testPostFilter_businessCode0000_noStatusChange() throws Exception {
filter.doPostFilter(GRP, ADPT, body("0000"), prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("2-6. HTTP 상태코드 범위(100~599)를 벗어난 값은 설정하지 않는다")
void testPostFilter_outOfRangeStatus_noStatusChange() throws Exception {
for (String value : new String[] { "0", "99", "600", "9999", "-200" }) {
reset(mockResponse);
filter.doPostFilter(GRP, ADPT, body(value), prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
}
@Test
@DisplayName("2-7. JSON 배열 응답이면 상태코드를 변경하지 않는다")
void testPostFilter_jsonArray_noStatusChange() throws Exception {
filter.doPostFilter(GRP, ADPT, "[{\"" + FIELD + "\":\"404\"}]", prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("2-8. 필드값이 JSON null이면 상태코드를 변경하지 않는다")
void testPostFilter_jsonNullValue_noStatusChange() throws Exception {
filter.doPostFilter(GRP, ADPT, "{\"" + FIELD + "\":null}", prop, mockRequest, mockResponse);
verify(mockResponse, never()).setStatus(anyInt());
}
// =========================================================================
// 3. 비정상 입력에서도 예외를 던지지 않는다 (거래 실패 방지)
// =========================================================================
@Test
@DisplayName("3-1. JSON 형식이 아닌 응답이어도 예외 없이 통과한다")
void testPostFilter_nonJsonBody_noException() throws Exception {
String xml = "<xml><rslt>0000</rslt></xml>";
Object result = assertDoesNotThrow(
() -> filter.doPostFilter(GRP, ADPT, xml, prop, mockRequest, mockResponse));
assertSame(xml, result);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("3-2. 깨진 JSON 응답이어도 예외 없이 통과한다")
void testPostFilter_malformedJson_noException() {
String broken = "{\"" + FIELD + "\":\"404\"";
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, broken, prop, mockRequest, mockResponse));
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("3-3. null 응답이어도 예외 없이 null을 그대로 반환한다")
void testPostFilter_nullMessage_noException() {
Object result = assertDoesNotThrow(
() -> filter.doPostFilter(GRP, ADPT, null, prop, mockRequest, mockResponse));
assertNull(result);
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("3-4. 빈 문자열 응답이어도 예외 없이 통과한다")
void testPostFilter_emptyBody_noException() {
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, "", prop, mockRequest, mockResponse));
verify(mockResponse, never()).setStatus(anyInt());
}
@Test
@DisplayName("3-5. 프로퍼티 그룹 미등록(PropManager가 null 반환)이어도 예외 없이 통과한다")
void testPostFilter_propGroupMissing_noException() {
when(mockPropManager.getProperty(anyString(), anyString())).thenReturn(null);
assertDoesNotThrow(() -> filter.doPostFilter(GRP, ADPT, body("404"), prop, mockRequest, mockResponse));
verify(mockResponse, never()).setStatus(anyInt());
}
// =========================================================================
// 4. doPreFilter / 현재 동작 고정 (개선 검토 대상)
// =========================================================================
@Test
@DisplayName("4-1. doPreFilter는 아무 것도 하지 않고 요청 메시지를 그대로 반환한다")
void testPreFilter_doesNothing() throws Exception {
String message = body("404");
Object result = filter.doPreFilter(GRP, ADPT, message, prop, mockRequest, mockResponse);
assertSame(message, result);
verify(mockResponse, never()).setStatus(anyInt());
verifyNoInteractions(mockPropManager);
}
@Test
@DisplayName("4-2. [확인필요] byte[] 응답은 JSON으로 파싱되지 않아 상태코드가 설정되지 않는다")
void testPostFilter_byteArray_notSupported() throws Exception {
byte[] msg = body("404").getBytes(StandardCharsets.UTF_8);
Object result = filter.doPostFilter(GRP, ADPT, msg, prop, mockRequest, mockResponse);
// JsonPathUtil.toTree 가 byte[] 를 toString() 처리하므로 "[B@..." 가 되어 파싱에 실패한다.
verify(mockResponse, never()).setStatus(anyInt());
assertSame(msg, result);
}
}
@@ -0,0 +1,263 @@
package com.eactive.eai.manage.tools;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
/**
* ToolsController 단위 테스트.
*
* <p>Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
* 응답 구조(success, data, message)를 검증한다.
*/
class ToolsControllerTest {
private ToolsController controller;
private ToolsService mockService;
@BeforeEach
void setUp() {
controller = new ToolsController();
mockService = mock(ToolsService.class);
ReflectionTestUtils.setField(controller, "toolsService", mockService);
}
private ToolsTextRequestDTO request(String text, String charset) {
ToolsTextRequestDTO dto = new ToolsTextRequestDTO();
dto.setText(text);
dto.setCharset(charset);
return dto;
}
@SuppressWarnings("unchecked")
private Map<String, Object> body(ResponseEntity<?> response) {
return (Map<String, Object>) response.getBody();
}
// =========================================================================
// base64
// =========================================================================
@Test
@DisplayName("base64Encode() — 정상 시 success=true, data에 결과 반환")
void base64Encode_success_returnsData() {
when(mockService.base64Encode("hello", null)).thenReturn("aGVsbG8=");
ResponseEntity<?> response = controller.base64Encode(request("hello", null));
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("aGVsbG8=", body.get("data"));
assertFalse(body.containsKey("message"));
}
@Test
@DisplayName("base64Decode() — 잘못된 입력이면 success=false, message 포함 (500 아님)")
void base64Decode_serviceThrows_successFalseWithMessage() {
when(mockService.base64Decode("not-valid!!", null))
.thenThrow(new IllegalArgumentException("Illegal base64 character"));
ResponseEntity<?> response = controller.base64Decode(request("not-valid!!", null));
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.FALSE, body.get("success"));
assertNotNull(body.get("message"));
assertFalse(body.containsKey("data"));
}
// =========================================================================
// hex
// =========================================================================
@Test
@DisplayName("hexEncode() — 정상 시 success=true, data에 결과 반환")
void hexEncode_success_returnsData() {
when(mockService.hexEncode("hello", null)).thenReturn("68656C6C6F");
Map<String, Object> body = body(controller.hexEncode(request("hello", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("68656C6C6F", body.get("data"));
}
@Test
@DisplayName("hexDecode() — 잘못된 Hex 입력이면 success=false, message 포함")
void hexDecode_serviceThrows_successFalseWithMessage() {
when(mockService.hexDecode("ZZ", null)).thenThrow(new IllegalArgumentException("invalid hex"));
Map<String, Object> body = body(controller.hexDecode(request("ZZ", null)));
assertEquals(Boolean.FALSE, body.get("success"));
assertNotNull(body.get("message"));
}
@Test
@DisplayName("hexEncode() — charset 파라미터를 서비스에 그대로 위임")
void hexEncode_withCharset_delegatesToService() {
when(mockService.hexEncode("한글", "EUC-KR")).thenReturn("C7D1B1DB");
Map<String, Object> body = body(controller.hexEncode(request("한글", "EUC-KR")));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("C7D1B1DB", body.get("data"));
verify(mockService).hexEncode("한글", "EUC-KR");
}
// =========================================================================
// damo
// =========================================================================
@Test
@DisplayName("damoEncrypt() — 정상 시 success=true, data에 암호문 반환")
void damoEncrypt_success_returnsData() {
when(mockService.damoEncrypt("홍길동")).thenReturn("ENC(홍길동)");
Map<String, Object> body = body(controller.damoEncrypt(request("홍길동", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("ENC(홍길동)", body.get("data"));
}
@Test
@DisplayName("damoDecrypt() — 정상 시 success=true, data에 평문 반환")
void damoDecrypt_success_returnsData() {
when(mockService.damoDecrypt("ENC(홍길동)")).thenReturn("홍길동");
Map<String, Object> body = body(controller.damoDecrypt(request("ENC(홍길동)", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("홍길동", body.get("data"));
}
@Test
@DisplayName("damoDecrypt() — 서비스 예외 발생 시 success=false, message 포함 (500 아님)")
void damoDecrypt_serviceThrows_successFalseWithMessage() {
when(mockService.damoDecrypt("broken")).thenThrow(new RuntimeException("DAMO decrypt failed"));
ResponseEntity<?> response = controller.damoDecrypt(request("broken", null));
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.FALSE, body.get("success"));
assertEquals("DAMO decrypt failed", body.get("message"));
}
// =========================================================================
// encryption-manager (EncryptionManager.encryptDBData/decryptDBData 확인용)
// =========================================================================
@Test
@DisplayName("encryptionManagerEncrypt() — 정상 시 success=true, data에 결과 반환")
void encryptionManagerEncrypt_success_returnsData() {
when(mockService.encryptionManagerEncrypt("홍길동")).thenReturn("ENCDB(홍길동)");
Map<String, Object> body = body(controller.encryptionManagerEncrypt(request("홍길동", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("ENCDB(홍길동)", body.get("data"));
}
@Test
@DisplayName("encryptionManagerEncrypt() — encryptYN=N 등으로 원문 그대로 반환되어도 success=true")
void encryptionManagerEncrypt_passthrough_stillSuccess() {
when(mockService.encryptionManagerEncrypt("홍길동")).thenReturn("홍길동");
Map<String, Object> body = body(controller.encryptionManagerEncrypt(request("홍길동", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("홍길동", body.get("data"));
}
@Test
@DisplayName("encryptionManagerDecrypt() — 정상 시 success=true, data에 결과 반환")
void encryptionManagerDecrypt_success_returnsData() {
when(mockService.encryptionManagerDecrypt("ENCDB(홍길동)")).thenReturn("홍길동");
Map<String, Object> body = body(controller.encryptionManagerDecrypt(request("ENCDB(홍길동)", null)));
assertEquals(Boolean.TRUE, body.get("success"));
assertEquals("홍길동", body.get("data"));
}
@Test
@DisplayName("encryptionManagerStatus() — 현재 encryptYN/솔루션명/활성화 여부 반환")
void encryptionManagerStatus_returnsCurrentConfig() {
EncryptionManagerStatusDTO status = new EncryptionManagerStatusDTO();
status.setEncryptYN("Y");
status.setDbEncryptSolutionName("DAMO");
status.setEncryptEnabled(true);
when(mockService.encryptionManagerStatus()).thenReturn(status);
ResponseEntity<?> response = controller.encryptionManagerStatus();
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(status, body.get("data"));
}
@Test
@DisplayName("encryptionManagerStatus() — 서비스 예외 발생 시 success=false, message 포함")
void encryptionManagerStatus_serviceThrows_successFalseWithMessage() {
when(mockService.encryptionManagerStatus()).thenThrow(new IllegalStateException("EncryptionManager not started"));
Map<String, Object> body = body(controller.encryptionManagerStatus());
assertEquals(Boolean.FALSE, body.get("success"));
assertEquals("EncryptionManager not started", body.get("message"));
}
// =========================================================================
// version
// =========================================================================
@Test
@DisplayName("getVersionInfo() — 정상 시 success=true, data에 VersionInfoDTO 반환")
void getVersionInfo_success_returnsData() {
VersionInfoDTO versionInfo = new VersionInfoDTO();
versionInfo.setVersion("4.5.1-SNAPSHOT");
versionInfo.setBuildTime("2026-07-22 10:00:00");
when(mockService.getVersionInfo()).thenReturn(versionInfo);
ResponseEntity<?> response = controller.getVersionInfo();
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.TRUE, body.get("success"));
assertSame(versionInfo, body.get("data"));
}
@Test
@DisplayName("getVersionInfo() — version.info 파일이 없으면 success=false, message 포함 (500 아님)")
void getVersionInfo_fileMissing_successFalseWithMessage() {
when(mockService.getVersionInfo()).thenReturn(null);
ResponseEntity<?> response = controller.getVersionInfo();
Map<String, Object> body = body(response);
assertEquals(200, response.getStatusCodeValue());
assertEquals(Boolean.FALSE, body.get("success"));
assertNotNull(body.get("message"));
assertFalse(body.containsKey("data"));
}
@Test
@DisplayName("getVersionInfo() — 서비스 예외 발생 시 success=false, message 포함")
void getVersionInfo_serviceThrows_successFalseWithMessage() {
when(mockService.getVersionInfo()).thenThrow(new RuntimeException("version.info 읽기 실패"));
Map<String, Object> body = body(controller.getVersionInfo());
assertEquals(Boolean.FALSE, body.get("success"));
assertEquals("version.info 읽기 실패", body.get("message"));
}
}
@@ -0,0 +1,112 @@
package com.eactive.eai.manage.tools;
import static org.junit.jupiter.api.Assertions.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.support.GenericApplicationContext;
import com.eactive.eai.agent.encryption.EncryptionManager;
import com.eactive.eai.common.util.ApplicationContextProvider;
/**
* ToolsService의 EncryptionManager 연동(encryptionManagerEncrypt/Decrypt/Status) 단위 테스트.
*
* <p>EncryptionManager.getInstance()는 ApplicationContextProvider를 통해 실제 Spring
* ApplicationContext에서 빈을 조회하므로, base64/hex/damo 단독 테스트(ToolsServiceTest)와 분리해
* 최소한의 GenericApplicationContext를 구성한다.
*
* <p>damo-manager.jar가 네이티브 DAMO 라이브러리 없이 로드되면 FAKE MODE(encrypt→Base64,
* decrypt→Base64 디코딩)로 동작하므로(EncryptionManagerTest 참고) DAMO 모드에서도 라운드트립
* 검증이 가능하다.
*/
class ToolsServiceEncryptionManagerTest {
private static GenericApplicationContext ctx;
private static EncryptionManager manager;
private final ToolsService service = new ToolsService();
@BeforeAll
static void setUpClass() throws Exception {
Constructor<EncryptionManager> ctor = EncryptionManager.class.getDeclaredConstructor();
ctor.setAccessible(true);
manager = ctor.newInstance();
ctx = new GenericApplicationContext();
ctx.getBeanFactory().registerSingleton("encryptionManager", manager);
ctx.registerBeanDefinition("applicationContextProvider",
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
.getBeanDefinition());
ctx.refresh();
}
@AfterAll
static void tearDownClass() {
if (ctx != null) ctx.close();
}
private void setField(String name, Object value) throws Exception {
Field f = EncryptionManager.class.getDeclaredField(name);
f.setAccessible(true);
f.set(manager, value);
}
// =========================================================================
// 1. encryptionManagerEncrypt / Decrypt
// =========================================================================
@Test
@DisplayName("1-1. encryptYN=Y, DAMO — encrypt/decrypt 라운드트립")
void testEncryptDecrypt_damoEnabled_roundTrip() throws Exception {
setField("encryptYN", "Y");
setField("dbEncryptSolutionName", "DAMO");
// DamoManager의 실제 동작(FAKE MODE로 Base64 변환 / 네이티브 라이브러리 미초기화 시 원문 그대로
// bypass)은 실행 환경에 따라 달라질 수 있어 encrypt 결과가 원문과 달라야 한다는 보장은 없다.
// 이 테스트는 encryptDBData → decryptDBData 라운드트립이 원문을 복원하는지만 검증한다.
String plain = "홍길동";
String encrypted = service.encryptionManagerEncrypt(plain);
assertEquals(plain, service.encryptionManagerDecrypt(encrypted));
}
@Test
@DisplayName("1-2. encryptYN=N — encrypt는 원문을 그대로 반환 (설정 비활성)")
void testEncrypt_disabled_returnsPlain() throws Exception {
setField("encryptYN", "N");
setField("dbEncryptSolutionName", "DAMO");
assertEquals("홍길동", service.encryptionManagerEncrypt("홍길동"));
}
// =========================================================================
// 2. encryptionManagerStatus
// =========================================================================
@Test
@DisplayName("2-1. encryptionManagerStatus() — 활성화 상태의 현재 필드값 반영")
void testStatus_enabled_reflectsCurrentFields() throws Exception {
setField("encryptYN", "Y");
setField("dbEncryptSolutionName", "DAMO");
EncryptionManagerStatusDTO status = service.encryptionManagerStatus();
assertEquals("Y", status.getEncryptYN());
assertEquals("DAMO", status.getDbEncryptSolutionName());
assertTrue(status.isEncryptEnabled());
}
@Test
@DisplayName("2-2. encryptionManagerStatus() — encryptYN=N이면 encryptEnabled=false")
void testStatus_disabled_encryptEnabledFalse() throws Exception {
setField("encryptYN", "N");
assertFalse(service.encryptionManagerStatus().isEncryptEnabled());
}
}
@@ -0,0 +1,221 @@
package com.eactive.eai.manage.tools;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* ToolsService 단위 테스트.
*
* <p>DAMO 관련 테스트는 damo-manager.jar가 네이티브 DAMO 라이브러리 없이 로드되는 FAKE MODE
* (encrypt → Base64 인코딩, decrypt → Base64 디코딩)에서 동작한다는 전제(EncryptionManagerTest 참고)로,
* 실행 환경과 무관하게 라운드트립이 성립함을 검증한다.
*/
class ToolsServiceTest {
private final ToolsService service = new ToolsService();
// =========================================================================
// 1. base64
// =========================================================================
@Test
@DisplayName("1-1. base64Encode/Decode — charset 생략 시 UTF-8 기준 라운드트립 (한글)")
void testBase64_defaultCharset_roundTrip_korean() {
String plain = "테스트 평문";
String encoded = service.base64Encode(plain, null);
assertNotEquals(plain, encoded);
String decoded = service.base64Decode(encoded, null);
assertEquals(plain, decoded);
}
@Test
@DisplayName("1-2. base64Encode — 알려진 UTF-8 값 검증")
void testBase64Encode_knownValue() {
assertEquals("aGVsbG8=", service.base64Encode("hello", null));
}
@Test
@DisplayName("1-3. base64Decode — 알려진 값 검증")
void testBase64Decode_knownValue() {
assertEquals("hello", service.base64Decode("aGVsbG8=", null));
}
@Test
@DisplayName("1-4. base64Encode/Decode — 명시적 charset(EUC-KR) 라운드트립")
void testBase64_explicitCharset_roundTrip() {
String plain = "한글테스트";
String encoded = service.base64Encode(plain, "EUC-KR");
String decoded = service.base64Decode(encoded, "EUC-KR");
assertEquals(plain, decoded);
}
@Test
@DisplayName("1-5. base64Decode — 잘못된 Base64 입력은 예외 발생")
void testBase64Decode_invalid_throwsException() {
assertThrows(IllegalArgumentException.class, () -> service.base64Decode("not-valid-base64!!", null));
}
// =========================================================================
// 2. hex
// =========================================================================
@Test
@DisplayName("2-1. hexEncode — 알려진 UTF-8 값 검증 (대문자 Hex)")
void testHexEncode_knownValue() {
assertEquals("68656C6C6F", service.hexEncode("hello", null));
}
@Test
@DisplayName("2-2. hexEncode/Decode — charset 생략 시 UTF-8 기준 라운드트립 (한글)")
void testHex_defaultCharset_roundTrip_korean() {
String plain = "테스트 평문";
String encoded = service.hexEncode(plain, null);
assertNotEquals(plain, encoded);
String decoded = service.hexDecode(encoded, null);
assertEquals(plain, decoded);
}
@Test
@DisplayName("2-3. hexDecode — 소문자 Hex 입력도 정상 디코딩")
void testHexDecode_lowerCaseHex() {
assertEquals("hello", service.hexDecode("68656c6c6f", null));
}
@Test
@DisplayName("2-4. hexDecode — 앞뒤 공백은 trim 후 디코딩")
void testHexDecode_trimsWhitespace() {
assertEquals("hello", service.hexDecode(" 68656C6C6F ", null));
}
@Test
@DisplayName("2-5. hexDecode — 홀수 길이 Hex는 예외 발생")
void testHexDecode_oddLength_throwsException() {
assertThrows(IllegalArgumentException.class, () -> service.hexDecode("ABC", null));
}
@Test
@DisplayName("2-6. hexDecode — Hex가 아닌 문자 포함 시 예외 발생")
void testHexDecode_invalidChars_throwsException() {
assertThrows(IllegalArgumentException.class, () -> service.hexDecode("ZZZZ", null));
}
// =========================================================================
// 3. damo
// =========================================================================
@Test
@DisplayName("3-1. damoEncrypt/Decrypt — 라운드트립 (한글)")
void testDamo_roundTrip_korean() {
// DamoManager의 실제 동작(FAKE MODE로 Base64 변환 / 네이티브 라이브러리 미초기화 시 원문 그대로
// bypass)은 실행 환경에 따라 달라질 수 있어 encrypt 결과가 원문과 달라야 한다는 보장은 없다.
// 라운드트립(암호화→복호화 시 원문 복원)만 검증한다.
String plain = "홍길동";
String encrypted = service.damoEncrypt(plain);
assertEquals(plain, service.damoDecrypt(encrypted));
}
@Test
@DisplayName("3-2. damoEncrypt/Decrypt — 라운드트립 (영문/숫자 혼합)")
void testDamo_roundTrip_alphanumeric() {
String plain = "test@example.com";
String encrypted = service.damoEncrypt(plain);
assertEquals(plain, service.damoDecrypt(encrypted));
}
@Test
@DisplayName("3-3. damoEncrypt/Decrypt — 여러 입력 일괄 라운드트립 검증")
void testDamo_roundTrip_multipleValues() {
String[] inputs = {
"홍길동",
"test@example.com",
"900101-1234567",
"special chars: !@#$%",
"긴문자열긴문자열긴문자열긴문자열긴문자열"
};
for (String input : inputs) {
String encrypted = service.damoEncrypt(input);
String decrypted = service.damoDecrypt(encrypted);
assertEquals(input, decrypted, "라운드트립 불일치: [" + input + "]");
}
}
@Test
@DisplayName("3-4. 서로 다른 인코딩 방식(charset 지정 hex)도 UTF-8 바이트 기준과 일치")
void testHexEncode_matchesManualUtf8Bytes() {
String plain = "abc";
byte[] expected = plain.getBytes(StandardCharsets.UTF_8);
StringBuilder sb = new StringBuilder();
for (byte b : expected) {
sb.append(String.format("%02X", b));
}
assertEquals(sb.toString(), service.hexEncode(plain, "UTF-8"));
}
// =========================================================================
// 4. version.info
// =========================================================================
@Test
@DisplayName("4-1. parseVersionInfo() — version/buildTime 파싱")
void testParseVersionInfo_parsesVersionAndBuildTime() throws Exception {
String content = "version=4.5.1-SNAPSHOT\nbuildTime=2026-07-22 10:00:00\n";
try (InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
VersionInfoDTO dto = service.parseVersionInfo(in);
assertEquals("4.5.1-SNAPSHOT", dto.getVersion());
assertEquals("2026-07-22 10:00:00", dto.getBuildTime());
}
}
@Test
@DisplayName("4-2. parseVersionInfo() — 알 수 없는 키만 있으면 필드는 null")
void testParseVersionInfo_missingKeys_returnsNullFields() throws Exception {
String content = "someOtherKey=value\n";
try (InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
VersionInfoDTO dto = service.parseVersionInfo(in);
assertNull(dto.getVersion());
assertNull(dto.getBuildTime());
}
}
@Test
@DisplayName("4-3. parseVersionInfo() — module.<이름> 키를 서브모듈 버전 맵으로 파싱")
void testParseVersionInfo_parsesModuleVersions() throws Exception {
String content = "version=20260722_개발배포-dirty\nbuildTime=2026-07-23 08:51:29\n"
+ "module.elink-online-common=20260722-1-gcb0a588\n"
+ "module.elink-online-core=20260618_개발배포\n";
try (InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
VersionInfoDTO dto = service.parseVersionInfo(in);
assertEquals("20260722_개발배포-dirty", dto.getVersion());
assertEquals("20260722-1-gcb0a588", dto.getModuleVersions().get("elink-online-common"));
assertEquals("20260618_개발배포", dto.getModuleVersions().get("elink-online-core"));
}
}
@Test
@DisplayName("4-4. parseVersionInfo() — module.* 키가 없으면 moduleVersions는 빈 맵")
void testParseVersionInfo_noModuleKeys_returnsEmptyModuleVersions() throws Exception {
String content = "version=4.5.1-SNAPSHOT\nbuildTime=2026-07-22 10:00:00\n";
try (InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
VersionInfoDTO dto = service.parseVersionInfo(in);
assertTrue(dto.getModuleVersions().isEmpty());
}
}
@Test
@DisplayName("4-5. getVersionInfo() — classpath에 리소스가 없으면(빌드 태스크 미실행) null 반환, 예외 아님")
void testGetVersionInfo_resourceMissing_returnsNullWithoutException() {
// 이 테스트 클래스 자체가 클래스패스 루트에 없는 리소스를 조회하는 정상 케이스를 보장하진 않지만,
// generateVersionInfo 태스크를 거치지 않은 환경(로컬 IDE 등)에서도 예외 없이 동작해야 한다.
assertDoesNotThrow(() -> service.getVersionInfo());
}
}