Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44093681b7 | |||
| a24b437f5c | |||
| 67dba6ca4a | |||
| f4d7ac2fad | |||
| 6ca653aa1e | |||
| 90941bff54 | |||
| 9e1f7391a0 | |||
| cb0a588a9c |
@@ -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);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -127,6 +127,9 @@ public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandle
|
||||
StandardMessage resStandardMessage = resEaiMsg.getStandardMessage();
|
||||
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()) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+424
@@ -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,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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user