Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e1f7391a0 | |||
| cb0a588a9c | |||
| afe2040b6e | |||
| d13278c374 | |||
| 98d815e659 | |||
| c118c88dce | |||
| 28a4becf1c | |||
| 8339efc752 | |||
| 61c9aa9864 |
@@ -0,0 +1,391 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "b7e2e1d0-3c1a-4b0a-9d2e-crypto-manage-0001",
|
||||
"name": "CryptoModuleManage API (/manage/crypto)",
|
||||
"description": "CryptoModuleManager 기반 암호화모듈 진단 및 암복호화 테스트 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. cryptoName / dynamicCryptoName 을 DB(crypto_module_config)에 실제 등록된 모듈명으로 변경\n3. DYNAMIC 모듈은 keyDerivParams 의 contextKey 값과 dynamicContextKey 변수가 일치해야 함 (예: X-Api-Enc-Key)\n\n주의: 이 API는 원본 암호키(encKeyHex/decKeyHex/ivHex)를 절대 응답에 포함하지 않는다. 진단 응답은 메타데이터와 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
|
||||
{ "key": "cryptoName", "value": "SAMPLE_STATIC", "type": "string", "description": "STATIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
||||
{ "key": "dynamicCryptoName", "value": "SAMPLE_DYNAMIC", "type": "string", "description": "DYNAMIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
||||
{ "key": "dynamicContextKey", "value": "X-Api-Enc-Key", "type": "string", "description": "DYNAMIC 모듈의 key_deriv_params.contextKey 값과 일치해야 함" },
|
||||
{ "key": "dynamicContextValue", "value": "clientKeyValue01", "type": "string" },
|
||||
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
|
||||
{ "key": "plainTextBase64", "value": "", "type": "string" },
|
||||
{ "key": "staticCipherTextBase64", "value": "", "type": "string" },
|
||||
{ "key": "dynamicCipherTextBase64", "value": "", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 진단 (Diagnostics)",
|
||||
"item": [
|
||||
{
|
||||
"name": "전체 암호화모듈 목록 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/list",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "list"]
|
||||
},
|
||||
"description": "등록된 전체 암호화모듈의 진단정보(메타데이터)를 반환한다. 원본 키는 포함되지 않는다."
|
||||
},
|
||||
"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.an('array'));",
|
||||
"pm.test('원본 키 필드가 응답에 없어야 함', () => {",
|
||||
" const body = pm.response.text();",
|
||||
" pm.expect(body).to.not.include('encKeyHex');",
|
||||
" pm.expect(body).to.not.include('decKeyHex');",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단일 암호화모듈 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}"]
|
||||
},
|
||||
"description": "cryptoName 하나의 진단정보를 조회한다. STATIC 모듈은 keyDerivStrategy가 null, strategyLoaded가 false로 내려온다."
|
||||
},
|
||||
"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('cryptoName 일치', () => pm.expect(json.data.cryptoName).to.eql(pm.collectionVariables.get('cryptoName')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DYNAMIC 모듈 조회 (전략 로드 확인)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}"]
|
||||
},
|
||||
"description": "DYNAMIC 모듈은 resolveStrategy를 통해 키 도출 전략(KeyDerivationStrategy) 클래스가 정상 로드되는지 strategyLoaded/strategyClassName으로 확인할 수 있다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('keySourceType = DYNAMIC', () => pm.expect(json.data.keySourceType).to.eql('DYNAMIC'));",
|
||||
"pm.test('strategyLoaded = true', () => pm.expect(json.data.strategyLoaded).to.eql(true));",
|
||||
"pm.test('strategyClassName 존재', () => pm.expect(json.data.strategyClassName).to.be.a('string'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "동적 키 캐시 목록 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/cache/dynamic-keys",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "cache", "dynamic-keys"]
|
||||
},
|
||||
"description": "DYNAMIC 키 방식 모듈이 캐시한 키의 캐시-키 목록(예: 'cryptoName:contextValue')을 반환한다. 원본 키 값이 아니다. 아래 'DYNAMIC 키' 폴더의 암호화 요청을 먼저 실행한 뒤 호출하면 목록이 채워진다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. STATIC 키 암복호화 테스트",
|
||||
"item": [
|
||||
{
|
||||
"name": "암호화 (encrypt)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "encrypt"]
|
||||
},
|
||||
"description": "STATIC 키 방식 모듈로 평문(Base64)을 암호화한다. runtimeContext 없이 호출한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const plainText = pm.collectionVariables.get('plainText');",
|
||||
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
||||
"pm.test('평문 base64와 달라야 함', () => pm.expect(json.data.cipherTextBase64).to.not.eql(pm.collectionVariables.get('plainTextBase64')));",
|
||||
"pm.collectionVariables.set('staticCipherTextBase64', json.data.cipherTextBase64);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{staticCipherTextBase64}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 staticCipherTextBase64를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
|
||||
},
|
||||
"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));",
|
||||
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. DYNAMIC 키 암복호화 테스트",
|
||||
"item": [
|
||||
{
|
||||
"name": "암호화 (encrypt, runtimeContext 포함)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "encrypt"]
|
||||
},
|
||||
"description": "DYNAMIC 키 방식 모듈로 runtimeContext(예: 헤더값)를 전달하여 키를 도출한 뒤 암호화한다. dynamicContextKey는 DB에 등록된 key_deriv_params.contextKey와 일치해야 한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const plainText = pm.collectionVariables.get('plainText');",
|
||||
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
||||
"pm.collectionVariables.set('dynamicCipherTextBase64', json.data.cipherTextBase64);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt, 동일 runtimeContext 필요)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "암호화 시 사용한 것과 동일한 runtimeContext를 전달해야 동일한 키가 도출되어 복호화에 성공한다. dynamicContextValue를 바꿔서 보내면 키가 달라져 복호화가 실패(예외)하는 것도 확인해볼 수 있다."
|
||||
},
|
||||
"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));",
|
||||
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. 에러 케이스",
|
||||
"item": [
|
||||
{
|
||||
"name": "미등록 cryptoName 조회 → success=false",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/NOT_REGISTERED_MODULE",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "NOT_REGISTERED_MODULE"]
|
||||
},
|
||||
"description": "존재하지 않는 cryptoName 조회 시 HTTP 200 + 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));",
|
||||
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
|
||||
"pm.test('message에 모듈명 포함', () => pm.expect(json.message).to.include('NOT_REGISTERED_MODULE'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "잘못된 Base64로 복호화 → success=false",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"not-valid-base64!!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "Base64 형식이 아닌 문자열을 전달하면 success=false로 처리되고 서버가 500으로 죽지 않는지 확인한다."
|
||||
},
|
||||
"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 = false', () => pm.expect(json.success).to.eql(false));",
|
||||
"pm.test('message 존재', () => pm.expect(json.message).to.be.a('string'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "다른 runtimeContext로 DYNAMIC 복호화 시도 → 실패 확인",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"differentContextValue\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "'3. DYNAMIC 키' 폴더의 암호화를 먼저 실행해 dynamicCipherTextBase64를 채운 뒤, 암호화 때와 다른 runtimeContext 값으로 복호화를 시도한다. 키가 달라 padding/복호화 오류로 success=false가 되는 것을 확인한다 (패딩 모드에 따라 우연히 성공할 수도 있으니 참고용)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"console.log('다른 컨텍스트 복호화 결과:', JSON.stringify(json));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "c3f4a1e0-9b2d-4a11-8e5a-session-cache-0001",
|
||||
"name": "SessionCacheManage API (/manage/session)",
|
||||
"description": "SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. 서버가 Ignite 캐시 백엔드로 기동된 경우에만 outbound-access-token 캐시가 채워짐 (Ehcache 백엔드는 success=false로 응답, 정상 동작임)\n3. 모든 요청은 조회 전용(GET)이며 실제 세션/캐시 데이터를 변경하지 않는다.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 캐시 요약 / 마스터 인스턴스",
|
||||
"item": [
|
||||
{
|
||||
"name": "전체 캐시 요약 (구현체별 XML)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/status",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "status"]
|
||||
},
|
||||
"description": "Ignite/Ehcache 구현체가 각자 형식으로 만든 전체 캐시 이름/크기 요약 텍스트(XML)를 반환한다."
|
||||
},
|
||||
"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는 문자열(XML)', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.test('Caches 루트 태그 포함', () => pm.expect(json.data).to.include('<Caches'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "현재 evict master 인스턴스 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/master-inst-id",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "master-inst-id"]
|
||||
},
|
||||
"description": "싱글 어댑터 처리를 담당하는 현재 master 서버 인스턴스명을 반환한다. 아직 선출되지 않았으면 data가 null일 수 있다."
|
||||
},
|
||||
"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));",
|
||||
"console.log('master instance id:', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. 개별 캐시 조회",
|
||||
"item": [
|
||||
{
|
||||
"name": "로그인(WebSocket) 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "login"]
|
||||
}
|
||||
},
|
||||
"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('name = loginSession', () => pm.expect(json.data.name).to.eql('loginSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
|
||||
"pm.test('size는 keys 개수와 일치', () => pm.expect(json.data.size).to.eql(json.data.keys.length));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HTTP 로그인 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/http-login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "http-login"]
|
||||
}
|
||||
},
|
||||
"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('name = httpLogin', () => pm.expect(json.data.name).to.eql('httpLogin'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "싱글 어댑터 evict-master 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/evict-master",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "evict-master"]
|
||||
}
|
||||
},
|
||||
"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('name = evictMasterCache', () => pm.expect(json.data.name).to.eql('evictMasterCache'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단말(ATM 등) 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/terminal",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "terminal"]
|
||||
}
|
||||
},
|
||||
"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('name = terminalSession', () => pm.expect(json.data.name).to.eql('terminalSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단말ID→사용자ID 변환 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/convert-user-id",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "convert-user-id"]
|
||||
}
|
||||
},
|
||||
"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('name = convertUserIdSession', () => pm.expect(json.data.name).to.eql('convertUserIdSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "소켓 세션 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/socket",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "socket"]
|
||||
}
|
||||
},
|
||||
"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('name = adapterSession', () => pm.expect(json.data.name).to.eql('adapterSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "아웃바운드 AccessToken 캐시 (Ignite 전용)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/outbound-access-token",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "outbound-access-token"]
|
||||
},
|
||||
"description": "Ignite 백엔드에서만 지원한다. Ehcache 백엔드로 기동된 서버라면 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('data.name 존재', () => pm.expect(json.data.name).to.be.a('string'));",
|
||||
" pm.test('data.keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
|
||||
"} else {",
|
||||
" pm.test('미지원 백엔드는 message 포함', () => pm.expect(json.message).to.be.a('string'));",
|
||||
" console.log('OutboundAccessToken 캐시 미지원 백엔드:', json.message);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. 전체 캐시 목록",
|
||||
"item": [
|
||||
{
|
||||
"name": "모든 캐시 한번에 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/all",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "all"]
|
||||
},
|
||||
"description": "login/http-login/evict-master/terminal/convert-user-id/socket 6종은 항상 포함되고, outbound-access-token은 Ignite 백엔드일 때만 추가되어 count가 6 또는 7이 된다."
|
||||
},
|
||||
"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.an('array'));",
|
||||
"pm.test('count는 6 또는 7 (백엔드에 따라 outbound-access-token 포함 여부 다름)', () => {",
|
||||
" pm.expect(json.count).to.be.oneOf([6, 7]);",
|
||||
"});",
|
||||
"pm.test('count와 data.length 일치', () => pm.expect(json.count).to.eql(json.data.length));",
|
||||
"json.data.forEach((cache) => {",
|
||||
" pm.test(`캐시 [${cache.name}] 는 keys 배열을 가짐`, () => pm.expect(cache.keys).to.be.an('array'));",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public class HttpStatusException extends Exception {
|
||||
|
||||
public String getCode() {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return String.format("Http Status: %d", status);
|
||||
return String.format("HttCd:%d", status);
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
|
||||
+8
-8
@@ -88,10 +88,17 @@ public class DeadlineAwareRetryExecutor {
|
||||
|
||||
if (!policy.shouldRetryOnStatus(request, status)) {
|
||||
// 성공 또는 재시도 불필요 → 호출부에서 close 책임
|
||||
log.debug("non-retryable response status={}, attempt={}", status, attempt + 1);
|
||||
log.info("non-retryable response status={}, attempt={}", status, attempt + 1);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ④ 마지막 시도였으면 종료
|
||||
if (attempt >= policy.getMaxRetries()) {
|
||||
// 최대 재처리에 도달하여, 재시도하지 않고, 응답 객체를 리턴한다.
|
||||
log.info("최대 재시도 횟수 도달 response status={}, attempt={}", status, attempt + 1);
|
||||
return response;
|
||||
}
|
||||
|
||||
// 재시도 대상
|
||||
log.warn("retryable status={}, attempt={}/{}",
|
||||
status, attempt + 1, policy.getMaxRetries());
|
||||
@@ -127,9 +134,6 @@ public class DeadlineAwareRetryExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// ④ 마지막 시도였으면 바로 종료
|
||||
if (attempt >= policy.getMaxRetries()) break;
|
||||
|
||||
// ⑤ 백오프 대기 전 남은 시간 재확인
|
||||
long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime);
|
||||
if (remainingBeforeWait <= policy.getBackoffMs()) {
|
||||
@@ -171,10 +175,6 @@ public class DeadlineAwareRetryExecutor {
|
||||
log.info("RequestConfig = {}", config);
|
||||
}
|
||||
|
||||
private boolean isSuccess(int status) {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
private void closeQuietly(CloseableHttpResponse response) {
|
||||
try {
|
||||
response.close();
|
||||
|
||||
+10
-1
@@ -14,6 +14,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
@@ -135,7 +136,15 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
|
||||
byte[] responseMessage = null;
|
||||
String responseString = "";
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) this.client.execute(method, context)) {
|
||||
|
||||
DeadlineAwareRetryExecutor executor =
|
||||
new DeadlineAwareRetryExecutor((CloseableHttpClient)this.client,
|
||||
vo.getConnectionTimeout(),
|
||||
vo.getTimeout());
|
||||
|
||||
// Properties에서 RetryPolicy 로드
|
||||
RetryPolicy policy = RetryPolicy.from(prop); // 설정 로드 방식에 맞게
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) executor.execute(method, context, policy)) {
|
||||
status = response.getCode();
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
responseString = new String(responseMessage, vo.getEncode());
|
||||
|
||||
+15
-17
@@ -862,26 +862,24 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
}
|
||||
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s", authorization));
|
||||
}
|
||||
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, String authorization, String authorizationHeaderName) throws Exception {
|
||||
if(StringUtils.isEmpty(authorizationHeaderName))
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, String authorization, String authorizationHeaderName)
|
||||
throws Exception {
|
||||
if (StringUtils.isEmpty(authorizationHeaderName))
|
||||
authorizationHeaderName = "Authorization";
|
||||
method.setHeader(authorizationHeaderName,
|
||||
String.format("%s", authorization));
|
||||
|
||||
method.setHeader(authorizationHeaderName, String.format("%s", authorization));
|
||||
}
|
||||
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken, String authorizationHeaderName) throws Exception {
|
||||
method.setHeader(authorizationHeaderName,
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
protected void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken,
|
||||
String authorizationHeaderName) throws Exception {
|
||||
if (StringUtils.isEmpty(authorizationHeaderName))
|
||||
authorizationHeaderName = "Authorization";
|
||||
|
||||
if (StringUtils.isEmpty(accessToken.getTokenType()))
|
||||
method.setHeader(authorizationHeaderName, String.format("%s", accessToken.getAccessToken()));
|
||||
else
|
||||
method.setHeader(authorizationHeaderName,
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
|
||||
@@ -65,7 +65,7 @@ public class RetryPolicy {
|
||||
|
||||
return new RetryPolicy(getInt(props, RETRY_MAX_RETRIES, 0), getLong(props, RETRY_BACKOFF_MS, 1000L), codes,
|
||||
getBoolean(props, RETRY_ON_CONNECT_FAIL, false), getBoolean(props, RETRY_ON_TIMEOUT, false),
|
||||
getBoolean(props, RETRY_IDEMPOTENT_ONLY, true));
|
||||
getBoolean(props, RETRY_IDEMPOTENT_ONLY, false));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@@ -85,4 +85,8 @@ public interface HttpAdapterServiceKey {
|
||||
|
||||
//응답 처리 용 표준 전문 오브젝트
|
||||
static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT";
|
||||
|
||||
// 어댑터별 인증 키 헤더 이름
|
||||
static final String ADAPTER_TOKEN_HEADER_NAME = "ADAPTER_TOKEN_HEADER_NAME";
|
||||
static final String ADAPTER_APIKEY_HEADER_NAME = "ADAPTER_APIKEY_HEADER_NAME";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.authserver.service.BearerTokenInfo;
|
||||
//import com.eactive.eai.authserver.vo.BearerTokenInfo;
|
||||
@@ -13,7 +34,6 @@ import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
import com.eactive.eai.common.stdmessage.STDMessageManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.StringUtil;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
@@ -25,27 +45,6 @@ import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jose.util.IOUtils;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
@@ -124,7 +123,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
switch (eaiMessage.getAuthType()){
|
||||
case "oauth":
|
||||
try {
|
||||
String token = extractBearerToken(request);
|
||||
String token = extractBearerToken(request, prop);
|
||||
SignedJWT signedJWT = SignedJWT.parse(token);
|
||||
|
||||
if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) {
|
||||
@@ -182,8 +181,23 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
}
|
||||
break;
|
||||
case "api_key":
|
||||
String apiKeyName = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
|
||||
String apiKey = request.getHeader(apiKeyName);
|
||||
// 기관별로 API Key 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_TOKEN_HEADER_NAME)을 우선 사용하고
|
||||
// 없으면 ApiConfig 전역 설정(api.key.name)을 사용. 콤마로 다중 헤더명 지정 시 순서대로 값이 있는 헤더를 사용
|
||||
String apiKeyNameConf = prop.getProperty(HttpAdapterServiceKey.ADAPTER_APIKEY_HEADER_NAME);
|
||||
if (StringUtils.isBlank(apiKeyNameConf)) {
|
||||
apiKeyNameConf = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
|
||||
}
|
||||
String[] apiKeyNames = org.springframework.util.StringUtils.tokenizeToStringArray(apiKeyNameConf, ",");
|
||||
String apiKeyName = null;
|
||||
String apiKey = null;
|
||||
for (String name : apiKeyNames) {
|
||||
String value = request.getHeader(name);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
apiKeyName = name;
|
||||
apiKey = value.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(apiKey == null){
|
||||
// QueryString으로 전달된 access_token 파라미터 확인
|
||||
String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key");
|
||||
@@ -191,14 +205,14 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
if (StringUtils.isNotBlank(queryApiKey)) {
|
||||
apiKey = queryApiKey.trim();
|
||||
} else {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyName+"\" in Http Header");
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyNameConf+"\" in Http Header");
|
||||
}
|
||||
}
|
||||
prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, apiKey);
|
||||
isPassScope = true;
|
||||
break;
|
||||
case "ca":
|
||||
String token = extractBearerToken(request);
|
||||
String token = extractBearerToken(request, prop);
|
||||
BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token);
|
||||
if ( bearerTokenInfo != null ) {
|
||||
if( bearerTokenInfo.isExpired() ) {
|
||||
@@ -319,8 +333,13 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String extractBearerToken(HttpServletRequest request) throws JwtAuthException {
|
||||
String tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
|
||||
public static String extractBearerToken(HttpServletRequest request, Properties prop) throws JwtAuthException {
|
||||
// 기관별로 토큰 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_APPKEY_HEADER_NAME)을 우선 사용하고
|
||||
// 없으면 ApiConfig 전역 설정(token.header.name)을 사용
|
||||
String tokenHeaderName = prop != null ? prop.getProperty(HttpAdapterServiceKey.ADAPTER_TOKEN_HEADER_NAME) : null;
|
||||
if (StringUtils.isBlank(tokenHeaderName)) {
|
||||
tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
|
||||
}
|
||||
String authorization = request.getHeader(tokenHeaderName);
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
// QueryString으로 전달된 access_token 파라미터 확인
|
||||
|
||||
@@ -104,8 +104,9 @@ public class InflowControlDAO extends BaseDAO {
|
||||
for (InflowControl inflowControl : inflowControls) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(inflowControl.getId().getName());
|
||||
vo.setThreshold(inflowControl.getThreshold());
|
||||
vo.setThresholdPerSecond(inflowControl.getThresholdpersecond());
|
||||
vo.setThreshold(inflowControl.getThreshold() == null ? 0 : inflowControl.getThreshold());
|
||||
vo.setThresholdPerSecond(
|
||||
inflowControl.getThresholdpersecond() == null ? 0 : inflowControl.getThresholdpersecond());
|
||||
vo.setThresholdTimeUnit(inflowControl.getThresholdtimeunit());
|
||||
vo.setActivate(!"0".equals(inflowControl.getUseyn()));
|
||||
targetList.put(vo.getName(), vo);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* CryptoModuleManager 진단정보 DTO.
|
||||
*
|
||||
* 원본 암호키(encKey/decKey) 및 IV는 절대 포함하지 않는다. 메타데이터와
|
||||
* 키 도출 전략(KeyDerivationStrategy) 로드 상태만 담는다.
|
||||
*/
|
||||
@Data
|
||||
public class CryptoModuleDiagnosticDTO {
|
||||
|
||||
String cryptoName;
|
||||
String cryptoDesc;
|
||||
String algType;
|
||||
String cipherMode;
|
||||
String padding;
|
||||
boolean hasIv;
|
||||
String keySourceType;
|
||||
String keyDerivStrategy;
|
||||
boolean strategyLoaded;
|
||||
String strategyClassName;
|
||||
String strategyLoadError;
|
||||
String cacheYn;
|
||||
Integer cacheTtlSec;
|
||||
String useYn;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.eactive.eai.common.security;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -153,6 +154,58 @@ public class CryptoModuleManager implements Lifecycle {
|
||||
return buildExtension(vo, iv, derivedKey.getEncKey(), derivedKey.getDecKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록된 전체 암호화모듈의 진단정보를 반환한다.
|
||||
* 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
|
||||
*/
|
||||
public List<CryptoModuleDiagnosticDTO> describeAll() {
|
||||
List<CryptoModuleDiagnosticDTO> result = new ArrayList<>();
|
||||
for (String cryptoName : configMap.keySet()) {
|
||||
result.add(describe(cryptoName));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 암호화모듈의 진단정보를 반환한다. DYNAMIC 키 방식인 경우 resolveStrategy를 통해
|
||||
* 전략 클래스 로드 성공 여부를 함께 확인한다. 원본 키(encKey/decKey/ivHex)는 포함하지 않는다.
|
||||
*/
|
||||
public CryptoModuleDiagnosticDTO describe(String cryptoName) {
|
||||
CryptoModuleConfigVO vo = getVO(cryptoName);
|
||||
|
||||
CryptoModuleDiagnosticDTO dto = new CryptoModuleDiagnosticDTO();
|
||||
dto.setCryptoName(vo.getCryptoName());
|
||||
dto.setCryptoDesc(vo.getCryptoDesc());
|
||||
dto.setAlgType(vo.getAlgType());
|
||||
dto.setCipherMode(vo.getCipherMode());
|
||||
dto.setPadding(vo.getPadding());
|
||||
dto.setHasIv(vo.getIvHex() != null);
|
||||
dto.setKeySourceType(vo.getKeySourceType());
|
||||
dto.setCacheYn(vo.getCacheYn());
|
||||
dto.setCacheTtlSec(vo.getCacheTtlSec());
|
||||
dto.setUseYn(vo.getUseYn());
|
||||
|
||||
if (!"STATIC".equalsIgnoreCase(vo.getKeySourceType()) && vo.getKeyDerivStrategy() != null) {
|
||||
dto.setKeyDerivStrategy(vo.getKeyDerivStrategy());
|
||||
try {
|
||||
KeyDerivationStrategy strategy = resolveStrategy(vo.getKeyDerivStrategy());
|
||||
dto.setStrategyLoaded(true);
|
||||
dto.setStrategyClassName(strategy.getClass().getName());
|
||||
} catch (Exception e) {
|
||||
dto.setStrategyLoaded(false);
|
||||
dto.setStrategyLoadError(e.getMessage());
|
||||
}
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 동적 키 캐시에 존재하는 캐시 키 목록을 반환한다. (전략별 buildCacheKey 결과이며, 원본 키 값이 아니다)
|
||||
*/
|
||||
public List<String> listDynamicCacheKeys() {
|
||||
return new ArrayList<>(dynamicKeyCache.keySet());
|
||||
}
|
||||
|
||||
private CryptoModuleConfigVO getVO(String cryptoName) {
|
||||
CryptoModuleConfigVO vo = configMap.get(cryptoName);
|
||||
if (vo == null) {
|
||||
|
||||
@@ -246,6 +246,8 @@ public abstract class SessionManager implements Lifecycle {
|
||||
|
||||
public abstract CacheInfoVO getSocketCache();
|
||||
|
||||
public abstract CacheInfoVO getOutboundAccessTokenCache();
|
||||
|
||||
public abstract String getMasterInstId();
|
||||
|
||||
// Convert terminalId to userId
|
||||
|
||||
@@ -887,7 +887,12 @@ public class SessionManagerForEhcache extends SessionManager {
|
||||
public CacheInfoVO getSocketCache() {
|
||||
return convert(cacheSocket);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public CacheInfoVO getOutboundAccessTokenCache() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putWebSocketTimeout(String key, SessionVO value) {
|
||||
cacheWebSocketTimeout.put(new Element(key, value));
|
||||
|
||||
@@ -818,6 +818,11 @@ public class SessionManagerForIgnite extends SessionManager {
|
||||
public CacheInfoVO getSocketCache() {
|
||||
return convert(cacheSocket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CacheInfoVO getOutboundAccessTokenCache() {
|
||||
return convert(cacheOutBoundAccessToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putWebSocketTimeout(String key, SessionVO value) {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.eactive.eai.manage.crypto;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
|
||||
|
||||
/**
|
||||
* 암호화모듈(CryptoModuleManager) 진단 및 암복호화 테스트 API.
|
||||
*
|
||||
* 원본 암호키(encKey/decKey/ivHex)는 응답에 포함하지 않는다. 메타데이터와
|
||||
* 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.
|
||||
*
|
||||
* GET /manage/crypto/list → 등록된 전체 암호화모듈 진단정보
|
||||
* GET /manage/crypto/{cryptoName} → 단일 암호화모듈 진단정보
|
||||
* GET /manage/crypto/cache/dynamic-keys → 동적 키 캐시 키 목록 (원본 키 아님)
|
||||
* POST /manage/crypto/{cryptoName}/test/encrypt → 테스트 암호화 (Base64 입출력)
|
||||
* POST /manage/crypto/{cryptoName}/test/decrypt → 테스트 복호화 (Base64 입출력)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/manage/crypto")
|
||||
public class CryptoModuleManageController {
|
||||
|
||||
@Autowired
|
||||
private CryptoModuleManageService cryptoModuleManageService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<?> listAll() {
|
||||
List<CryptoModuleDiagnosticDTO> list = cryptoModuleManageService.listAll();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("data", list);
|
||||
result.put("count", list.size());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{cryptoName}")
|
||||
public ResponseEntity<?> get(@PathVariable String cryptoName) {
|
||||
return respond(() -> cryptoModuleManageService.get(cryptoName));
|
||||
}
|
||||
|
||||
@GetMapping("/cache/dynamic-keys")
|
||||
public ResponseEntity<?> listDynamicCacheKeys() {
|
||||
List<String> keys = cryptoModuleManageService.listDynamicCacheKeys();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("data", keys);
|
||||
result.put("count", keys.size());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@PostMapping("/{cryptoName}/test/encrypt")
|
||||
public ResponseEntity<?> testEncrypt(@PathVariable String cryptoName, @RequestBody CryptoTestRequestDTO request) {
|
||||
return respond(() -> {
|
||||
String cipherTextBase64 = cryptoModuleManageService.testEncrypt(cryptoName,
|
||||
request.getRuntimeContext(), request.getPlainTextBase64(), request.getAadBase64());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("cipherTextBase64", cipherTextBase64);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
@PostMapping("/{cryptoName}/test/decrypt")
|
||||
public ResponseEntity<?> testDecrypt(@PathVariable String cryptoName, @RequestBody CryptoTestRequestDTO request) {
|
||||
return respond(() -> {
|
||||
String plainTextBase64 = cryptoModuleManageService.testDecrypt(cryptoName,
|
||||
request.getRuntimeContext(), request.getCipherTextBase64(), request.getAadBase64());
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("plainTextBase64", plainTextBase64);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
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(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.eactive.eai.manage.crypto;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
|
||||
import com.eactive.eai.common.security.CryptoModuleExtension;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
|
||||
/**
|
||||
* CryptoModuleManager 진단 및 암복호화 테스트를 위임하는 서비스.
|
||||
*
|
||||
* createExtension(cryptoName, runtimeContext) 는 STATIC 키 방식인 경우 내부에서
|
||||
* createExtension(cryptoName) 으로 위임하므로, 테스트 시 runtimeContext 유무와 무관하게
|
||||
* 동일 메서드를 사용한다.
|
||||
*/
|
||||
@Service
|
||||
public class CryptoModuleManageService {
|
||||
|
||||
public List<CryptoModuleDiagnosticDTO> listAll() {
|
||||
return CryptoModuleManager.getInstance().describeAll();
|
||||
}
|
||||
|
||||
public CryptoModuleDiagnosticDTO get(String cryptoName) {
|
||||
return CryptoModuleManager.getInstance().describe(cryptoName);
|
||||
}
|
||||
|
||||
public List<String> listDynamicCacheKeys() {
|
||||
return CryptoModuleManager.getInstance().listDynamicCacheKeys();
|
||||
}
|
||||
|
||||
public String testEncrypt(String cryptoName, Map<String, String> runtimeContext,
|
||||
String plainTextBase64, String aadBase64) throws Exception {
|
||||
byte[] plain = Base64.getDecoder().decode(plainTextBase64);
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance()
|
||||
.createExtension(cryptoName, runtimeContext(runtimeContext));
|
||||
byte[] cipher = aadBase64 != null
|
||||
? ext.encrypt(plain, Base64.getDecoder().decode(aadBase64))
|
||||
: ext.encrypt(plain);
|
||||
return Base64.getEncoder().encodeToString(cipher);
|
||||
}
|
||||
|
||||
public String testDecrypt(String cryptoName, Map<String, String> runtimeContext,
|
||||
String cipherTextBase64, String aadBase64) throws Exception {
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(cipherTextBase64);
|
||||
CryptoModuleExtension ext = CryptoModuleManager.getInstance()
|
||||
.createExtension(cryptoName, runtimeContext(runtimeContext));
|
||||
byte[] plain = aadBase64 != null
|
||||
? ext.decrypt(cipherBytes, Base64.getDecoder().decode(aadBase64))
|
||||
: ext.decrypt(cipherBytes);
|
||||
return Base64.getEncoder().encodeToString(plain);
|
||||
}
|
||||
|
||||
private Map<String, String> runtimeContext(Map<String, String> runtimeContext) {
|
||||
return runtimeContext != null ? runtimeContext : Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.eactive.eai.manage.crypto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 암복호화 테스트 요청 DTO. 평문/암문은 Base64로 주고받는다.
|
||||
*/
|
||||
@Data
|
||||
public class CryptoTestRequestDTO {
|
||||
|
||||
String plainTextBase64;
|
||||
String cipherTextBase64;
|
||||
String aadBase64;
|
||||
Map<String, String> runtimeContext;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.eactive.eai.manage.session;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.eactive.eai.common.session.CacheInfoVO;
|
||||
|
||||
/**
|
||||
* SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 상태 모니터링 API.
|
||||
*
|
||||
* GET /manage/session/cache/status → 전체 캐시 요약 (구현체별 XML 텍스트, Ignite/Ehcache)
|
||||
* GET /manage/session/cache/all → 개별 캐시(login/http-login/evict-master/terminal/convert-user-id/socket) 목록
|
||||
* GET /manage/session/cache/login → 로그인(WebSocket) 캐시
|
||||
* GET /manage/session/cache/http-login → HTTP 로그인 캐시
|
||||
* GET /manage/session/cache/evict-master → 싱글 어댑터 evict master 캐시
|
||||
* GET /manage/session/cache/terminal → 단말(ATM 등) 캐시
|
||||
* GET /manage/session/cache/convert-user-id → 단말ID → 사용자ID 변환 캐시
|
||||
* GET /manage/session/cache/socket → 소켓 세션 캐시
|
||||
* GET /manage/session/cache/outbound-access-token → 아웃바운드 AccessTokenVO 캐시 (Ignite 백엔드에서만 지원)
|
||||
* GET /manage/session/master-inst-id → 현재 evict master 인스턴스명
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/manage/session")
|
||||
public class SessionCacheManageController {
|
||||
|
||||
@Autowired
|
||||
private SessionCacheManageService sessionCacheManageService;
|
||||
|
||||
@GetMapping("/cache/status")
|
||||
public ResponseEntity<?> getCacheStatus() {
|
||||
return ok(sessionCacheManageService.getCacheStatus());
|
||||
}
|
||||
|
||||
@GetMapping("/master-inst-id")
|
||||
public ResponseEntity<?> getMasterInstId() {
|
||||
return ok(sessionCacheManageService.getMasterInstId());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/all")
|
||||
public ResponseEntity<?> getAllCaches() {
|
||||
List<CacheInfoVO> list = sessionCacheManageService.getAllCaches();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("data", list);
|
||||
result.put("count", list.size());
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/cache/login")
|
||||
public ResponseEntity<?> getLoginCache() {
|
||||
return ok(sessionCacheManageService.getLoginCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/http-login")
|
||||
public ResponseEntity<?> getHttpLoginCache() {
|
||||
return ok(sessionCacheManageService.getHttpLoginCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/evict-master")
|
||||
public ResponseEntity<?> getEvictMasterCache() {
|
||||
return ok(sessionCacheManageService.getEvictMasterCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/terminal")
|
||||
public ResponseEntity<?> getTerminalCache() {
|
||||
return ok(sessionCacheManageService.getTerminalCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/convert-user-id")
|
||||
public ResponseEntity<?> getConvertUserIdCache() {
|
||||
return ok(sessionCacheManageService.getConvertUserIdCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/socket")
|
||||
public ResponseEntity<?> getSocketCache() {
|
||||
return ok(sessionCacheManageService.getSocketCache());
|
||||
}
|
||||
|
||||
@GetMapping("/cache/outbound-access-token")
|
||||
public ResponseEntity<?> getOutboundAccessTokenCache() {
|
||||
try {
|
||||
return ok(sessionCacheManageService.getOutboundAccessTokenCache());
|
||||
} catch (UnsupportedOperationException e) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", false);
|
||||
result.put("message", "현재 SessionManager 백엔드는 OutboundAccessToken 캐시를 지원하지 않습니다.");
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<?> ok(Object data) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", true);
|
||||
result.put("data", data);
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.eactive.eai.manage.session;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.session.CacheInfoVO;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
|
||||
/**
|
||||
* SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회를 위임하는 서비스.
|
||||
*
|
||||
* SessionManager.getInstance() 는 설정(session_manager.cacheType)에 따라 Ignite/Ehcache
|
||||
* 구현체 중 하나의 싱글턴을 반환하며, 두 구현체 모두 아래 조회 메서드를 지원한다.
|
||||
*/
|
||||
@Service
|
||||
public class SessionCacheManageService {
|
||||
|
||||
public String getCacheStatus() {
|
||||
return SessionManager.getInstance().getCacheStatus();
|
||||
}
|
||||
|
||||
public String getMasterInstId() {
|
||||
return SessionManager.getInstance().getMasterInstId();
|
||||
}
|
||||
|
||||
public CacheInfoVO getLoginCache() {
|
||||
return SessionManager.getInstance().getLoginCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getHttpLoginCache() {
|
||||
return SessionManager.getInstance().getHttpLoginCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getEvictMasterCache() {
|
||||
return SessionManager.getInstance().getEvictMasterCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getTerminalCache() {
|
||||
return SessionManager.getInstance().getTerminalCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getConvertUserIdCache() {
|
||||
return SessionManager.getInstance().getConvertUserIdCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getSocketCache() {
|
||||
return SessionManager.getInstance().getSocketCache();
|
||||
}
|
||||
|
||||
public CacheInfoVO getOutboundAccessTokenCache() {
|
||||
return SessionManager.getInstance().getOutboundAccessTokenCache();
|
||||
}
|
||||
|
||||
public List<CacheInfoVO> getAllCaches() {
|
||||
List<CacheInfoVO> result = new ArrayList<>();
|
||||
result.add(getLoginCache());
|
||||
result.add(getHttpLoginCache());
|
||||
result.add(getEvictMasterCache());
|
||||
result.add(getTerminalCache());
|
||||
result.add(getConvertUserIdCache());
|
||||
result.add(getSocketCache());
|
||||
try {
|
||||
result.add(getOutboundAccessTokenCache());
|
||||
} catch (UnsupportedOperationException e) {
|
||||
// Ehcache 백엔드는 OutboundAccessToken 캐시를 지원하지 않는다.
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -56,7 +56,10 @@ public class OAuth2AccessTokenVO implements AccessTokenVO, Serializable
|
||||
}
|
||||
|
||||
public void setTokenType(String tokenType) {
|
||||
this.tokenType = tokenType;
|
||||
if(tokenType.equalsIgnoreCase(BEARER_TYPE))
|
||||
this.tokenType = BEARER_TYPE;
|
||||
else
|
||||
this.tokenType = tokenType;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -295,6 +296,97 @@ class CryptoModuleManagerTest {
|
||||
"비활성화된 모듈은 configMap에서 제거되어야 한다");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 5. 진단정보 — describe / describeAll / listDynamicCacheKeys
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("5-1. describe() STATIC — keyDerivStrategy 없음, 원본 키 필드 없음")
|
||||
void testDescribe_static_noStrategyInfo() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("DESC_STATIC", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_STATIC");
|
||||
|
||||
assertEquals("DESC_STATIC", dto.getCryptoName());
|
||||
assertEquals("STATIC", dto.getKeySourceType());
|
||||
assertTrue(dto.isHasIv());
|
||||
assertNull(dto.getKeyDerivStrategy(), "STATIC 모듈은 keyDerivStrategy가 없어야 한다");
|
||||
assertFalse(dto.isStrategyLoaded(), "STATIC 모듈은 전략을 로드하지 않는다");
|
||||
assertFalse(dto.toString().contains(ENC_KEY_HEX), "원본 키(hex)가 진단정보에 노출되면 안 된다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-2. describe() DYNAMIC 정상 FQCN — strategyLoaded=true, strategyClassName 존재")
|
||||
void testDescribe_dynamic_validFqcn_strategyLoaded() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DESC_DYN", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_DYN");
|
||||
|
||||
assertEquals("DYNAMIC", dto.getKeySourceType());
|
||||
assertEquals("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy",
|
||||
dto.getKeyDerivStrategy());
|
||||
assertTrue(dto.isStrategyLoaded());
|
||||
assertEquals("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy",
|
||||
dto.getStrategyClassName());
|
||||
assertNull(dto.getStrategyLoadError());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-3. describe() DYNAMIC 잘못된 FQCN — strategyLoaded=false, strategyLoadError 존재")
|
||||
void testDescribe_dynamic_invalidFqcn_strategyLoadFailed() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DESC_BAD_FQCN", "AES", "CBC", "PKCS5Padding");
|
||||
config.setKeyDerivStrategy("com.example.NonExistentStrategy");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
CryptoModuleDiagnosticDTO dto = manager.describe("DESC_BAD_FQCN");
|
||||
|
||||
assertFalse(dto.isStrategyLoaded());
|
||||
assertNotNull(dto.getStrategyLoadError());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-4. describe() 미등록 cryptoName — IllegalArgumentException")
|
||||
void testDescribe_unknownName_throwsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> manager.describe("UNKNOWN_MODULE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-5. describeAll() — 등록된 모듈 수만큼 반환")
|
||||
void testDescribeAll_returnsAllRegisteredModules() throws Exception {
|
||||
CryptoModuleConfig c1 = buildStaticConfig("DESC_ALL_1", "AES", "CBC", "PKCS5Padding");
|
||||
CryptoModuleConfig c2 = buildDynamicConfig("DESC_ALL_2", "ARIA", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(c1, c2));
|
||||
reload();
|
||||
|
||||
List<CryptoModuleDiagnosticDTO> all = manager.describeAll();
|
||||
|
||||
assertEquals(2, all.size());
|
||||
assertTrue(all.stream().anyMatch(d -> "DESC_ALL_1".equals(d.getCryptoName())));
|
||||
assertTrue(all.stream().anyMatch(d -> "DESC_ALL_2".equals(d.getCryptoName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("5-6. listDynamicCacheKeys() — DYNAMIC 키 생성 후 캐시키 노출 (원본 키 아님)")
|
||||
void testListDynamicCacheKeys_afterCreateExtension_containsCacheKey() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("DESC_CACHE_KEYS", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
manager.createExtension("DESC_CACHE_KEYS", runtimeCtx);
|
||||
|
||||
List<String> cacheKeys = manager.listDynamicCacheKeys();
|
||||
|
||||
assertEquals(1, cacheKeys.size());
|
||||
assertEquals("DESC_CACHE_KEYS:clientKeyValue01", cacheKeys.get(0));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.eactive.eai.manage.crypto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
|
||||
|
||||
/**
|
||||
* CryptoModuleManageController 단위 테스트.
|
||||
*
|
||||
* <p>Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
|
||||
* 응답 구조(success, data, count, message)를 검증한다.
|
||||
*/
|
||||
class CryptoModuleManageControllerTest {
|
||||
|
||||
private CryptoModuleManageController controller;
|
||||
private CryptoModuleManageService mockService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new CryptoModuleManageController();
|
||||
mockService = mock(CryptoModuleManageService.class);
|
||||
ReflectionTestUtils.setField(controller, "cryptoModuleManageService", mockService);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private CryptoModuleDiagnosticDTO makeDTO(String name, String keySourceType) {
|
||||
CryptoModuleDiagnosticDTO dto = new CryptoModuleDiagnosticDTO();
|
||||
dto.setCryptoName(name);
|
||||
dto.setKeySourceType(keySourceType);
|
||||
dto.setAlgType("AES");
|
||||
dto.setCipherMode("CBC");
|
||||
return dto;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> body(ResponseEntity<?> response) {
|
||||
return (Map<String, Object>) response.getBody();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 목록 조회 — listAll
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("listAll() — 등록된 모듈 목록과 개수 반환")
|
||||
void listAll_returnsListAndCount() {
|
||||
List<CryptoModuleDiagnosticDTO> list = Arrays.asList(
|
||||
makeDTO("MOD_A", "STATIC"),
|
||||
makeDTO("MOD_B", "DYNAMIC")
|
||||
);
|
||||
when(mockService.listAll()).thenReturn(list);
|
||||
|
||||
Map<String, Object> body = body(controller.listAll());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(list, body.get("data"));
|
||||
assertEquals(2, body.get("count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listAll() — 빈 목록이면 count=0")
|
||||
void listAll_emptyList_countZero() {
|
||||
when(mockService.listAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Map<String, Object> body = body(controller.listAll());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertEquals(0, body.get("count"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 단일 조회 — get
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("get() — 정상 조회 시 success=true, data 포함")
|
||||
void get_found_successTrueAndDataPresent() {
|
||||
CryptoModuleDiagnosticDTO dto = makeDTO("MOD_A", "STATIC");
|
||||
when(mockService.get("MOD_A")).thenReturn(dto);
|
||||
|
||||
ResponseEntity<?> response = controller.get("MOD_A");
|
||||
Map<String, Object> body = body(response);
|
||||
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(dto, body.get("data"));
|
||||
assertFalse(body.containsKey("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("get() — 미등록 cryptoName 예외 발생 시 success=false, message에 원인 포함")
|
||||
void get_unknownName_successFalseWithMessage() {
|
||||
when(mockService.get("GHOST")).thenThrow(new IllegalArgumentException("암호화모듈 미등록: GHOST"));
|
||||
|
||||
Map<String, Object> body = body(controller.get("GHOST"));
|
||||
|
||||
assertEquals(Boolean.FALSE, body.get("success"));
|
||||
assertTrue(body.get("message").toString().contains("GHOST"));
|
||||
assertFalse(body.containsKey("data"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 동적 키 캐시 목록 — listDynamicCacheKeys
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("listDynamicCacheKeys() — 캐시키 목록과 개수 반환")
|
||||
void listDynamicCacheKeys_returnsListAndCount() {
|
||||
List<String> keys = Arrays.asList("MOD_A:ctx1", "MOD_A:ctx2");
|
||||
when(mockService.listDynamicCacheKeys()).thenReturn(keys);
|
||||
|
||||
Map<String, Object> body = body(controller.listDynamicCacheKeys());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(keys, body.get("data"));
|
||||
assertEquals(2, body.get("count"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 암복호화 테스트 — testEncrypt / testDecrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("testEncrypt() — 정상 암호화 시 success=true, cipherTextBase64 포함")
|
||||
void testEncrypt_success_returnsCipherTextBase64() throws Exception {
|
||||
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
|
||||
request.setPlainTextBase64("cGxhaW4=");
|
||||
when(mockService.testEncrypt(eq("MOD_A"), isNull(), eq("cGxhaW4="), isNull()))
|
||||
.thenReturn("Y2lwaGVy");
|
||||
|
||||
Map<String, Object> body = body(controller.testEncrypt("MOD_A", request));
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) body.get("data");
|
||||
assertEquals("Y2lwaGVy", data.get("cipherTextBase64"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("testEncrypt() — 서비스 예외 발생 시 success=false, message 포함")
|
||||
void testEncrypt_serviceThrows_successFalseWithMessage() throws Exception {
|
||||
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
|
||||
request.setPlainTextBase64("not-valid-base64!!");
|
||||
when(mockService.testEncrypt(anyString(), any(), anyString(), any()))
|
||||
.thenThrow(new IllegalArgumentException("Illegal base64 character"));
|
||||
|
||||
Map<String, Object> body = body(controller.testEncrypt("MOD_A", request));
|
||||
|
||||
assertEquals(Boolean.FALSE, body.get("success"));
|
||||
assertNotNull(body.get("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("testDecrypt() — runtimeContext 전달 시 서비스에 그대로 위임")
|
||||
void testDecrypt_withRuntimeContext_delegatesToService() throws Exception {
|
||||
Map<String, String> runtimeContext = new HashMap<>();
|
||||
runtimeContext.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
|
||||
CryptoTestRequestDTO request = new CryptoTestRequestDTO();
|
||||
request.setCipherTextBase64("Y2lwaGVy");
|
||||
request.setRuntimeContext(runtimeContext);
|
||||
when(mockService.testDecrypt("MOD_B", runtimeContext, "Y2lwaGVy", null))
|
||||
.thenReturn("cGxhaW4=");
|
||||
|
||||
Map<String, Object> body = body(controller.testDecrypt("MOD_B", request));
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> data = (Map<String, Object>) body.get("data");
|
||||
assertEquals("cGxhaW4=", data.get("plainTextBase64"));
|
||||
verify(mockService).testDecrypt("MOD_B", runtimeContext, "Y2lwaGVy", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package com.eactive.eai.manage.crypto;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
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.Test;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import com.eactive.eai.common.hsm.HsmCryptoService;
|
||||
import com.eactive.eai.common.security.CryptoModuleConfigVO;
|
||||
import com.eactive.eai.common.security.CryptoModuleDiagnosticDTO;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
import com.eactive.eai.common.security.loader.CryptoModuleConfigLoader;
|
||||
import com.eactive.eai.common.security.mapper.CryptoModuleConfigMapper;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.data.entity.onl.security.CryptoModuleConfig;
|
||||
|
||||
/**
|
||||
* CryptoModuleManageService 단위 테스트.
|
||||
*
|
||||
* <p>DB / 실 Spring 컨텍스트 없이 Mock으로 동작한다. CryptoModuleManager는 실제 인스턴스를
|
||||
* 사용하며(싱글턴 getInstance() 경유), HSM 및 DB 로더만 Mock 처리한다.
|
||||
*/
|
||||
class CryptoModuleManageServiceTest {
|
||||
|
||||
private static final byte[] KEY_128 = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] IV_16 = "abcdef0123456789".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String ENC_KEY_HEX = toHex(KEY_128);
|
||||
private static final String IV_HEX = toHex(IV_16);
|
||||
|
||||
private static GenericApplicationContext ctx;
|
||||
private static CryptoModuleConfigLoader mockLoader;
|
||||
private static HsmCryptoService mockHsmCryptoService;
|
||||
private static CryptoModuleManager manager;
|
||||
|
||||
private final CryptoModuleManageService service = new CryptoModuleManageService();
|
||||
|
||||
@BeforeAll
|
||||
static void setUpClass() throws Exception {
|
||||
mockLoader = mock(CryptoModuleConfigLoader.class);
|
||||
mockHsmCryptoService = mock(HsmCryptoService.class);
|
||||
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
|
||||
|
||||
Constructor<CryptoModuleManager> ctor = CryptoModuleManager.class.getDeclaredConstructor();
|
||||
ctor.setAccessible(true);
|
||||
manager = ctor.newInstance();
|
||||
|
||||
ctx = new GenericApplicationContext();
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigLoader", mockLoader);
|
||||
ctx.getBeanFactory().registerSingleton("hsmCryptoService", mockHsmCryptoService);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleManager", manager);
|
||||
ctx.getBeanFactory().registerSingleton("cryptoModuleConfigMapper", testMapper());
|
||||
ctx.registerBeanDefinition("applicationContextProvider",
|
||||
BeanDefinitionBuilder.genericBeanDefinition(ApplicationContextProvider.class)
|
||||
.getBeanDefinition());
|
||||
ctx.refresh();
|
||||
|
||||
manager.start();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDownClass() {
|
||||
if (ctx != null) ctx.close();
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void resetMocks() throws Exception {
|
||||
reset(mockLoader);
|
||||
when(mockLoader.findAll()).thenReturn(Collections.emptyList());
|
||||
reload();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. 진단정보 위임 — listAll / get
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. listAll() — CryptoModuleManager.describeAll() 위임")
|
||||
void testListAll_delegatesToManager() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("SVC_LIST", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
List<CryptoModuleDiagnosticDTO> result = service.listAll();
|
||||
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("SVC_LIST", result.get(0).getCryptoName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. get() — 미등록 cryptoName은 IllegalArgumentException 전파")
|
||||
void testGet_unknownName_throwsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> service.get("UNKNOWN_MODULE"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. 동적 키 캐시 목록 — listDynamicCacheKeys
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. listDynamicCacheKeys() — DYNAMIC 암호화 테스트 후 캐시키 노출")
|
||||
void testListDynamicCacheKeys_afterTestEncrypt_containsCacheKey() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("SVC_CACHE_KEYS", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
String plainB64 = Base64.getEncoder().encodeToString("hello".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
service.testEncrypt("SVC_CACHE_KEYS", runtimeCtx, plainB64, null);
|
||||
|
||||
assertEquals(Collections.singletonList("SVC_CACHE_KEYS:clientKeyValue01"), service.listDynamicCacheKeys());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 3. 암복호화 테스트 — testEncrypt / testDecrypt
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("3-1. STATIC — 암호화 후 복호화 라운드트립 (Base64)")
|
||||
void testEncryptDecrypt_static_roundTrip() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("SVC_STATIC_RT", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
String plainText = "테스트 평문";
|
||||
String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String cipherB64 = service.testEncrypt("SVC_STATIC_RT", null, plainB64, null);
|
||||
assertNotNull(cipherB64);
|
||||
assertNotEquals(plainB64, cipherB64);
|
||||
|
||||
String decryptedB64 = service.testDecrypt("SVC_STATIC_RT", null, cipherB64, null);
|
||||
assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-2. DYNAMIC — runtimeContext 전달 시 암복호화 라운드트립 (Base64)")
|
||||
void testEncryptDecrypt_dynamic_roundTrip() throws Exception {
|
||||
CryptoModuleConfig config = buildDynamicConfig("SVC_DYNAMIC_RT", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
Map<String, String> runtimeCtx = new HashMap<>();
|
||||
runtimeCtx.put("X-Api-Enc-Key", "clientKeyValue01");
|
||||
|
||||
String plainText = "DYNAMIC 키 테스트";
|
||||
String plainB64 = Base64.getEncoder().encodeToString(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String cipherB64 = service.testEncrypt("SVC_DYNAMIC_RT", runtimeCtx, plainB64, null);
|
||||
String decryptedB64 = service.testDecrypt("SVC_DYNAMIC_RT", runtimeCtx, cipherB64, null);
|
||||
|
||||
assertEquals(plainText, new String(Base64.getDecoder().decode(decryptedB64), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-3. testDecrypt() — 잘못된 Base64 입력은 예외 전파")
|
||||
void testDecrypt_invalidBase64_throwsException() throws Exception {
|
||||
CryptoModuleConfig config = buildStaticConfig("SVC_BAD_B64", "AES", "CBC", "PKCS5Padding");
|
||||
when(mockLoader.findAll()).thenReturn(Arrays.asList(config));
|
||||
reload();
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.testDecrypt("SVC_BAD_B64", null, "not-valid-base64!!", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3-4. testEncrypt() — 미등록 cryptoName은 예외 전파")
|
||||
void testEncrypt_unknownName_throwsException() {
|
||||
String plainB64 = Base64.getEncoder().encodeToString("x".getBytes(StandardCharsets.UTF_8));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> service.testEncrypt("UNKNOWN_MODULE", null, plainB64, null));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 헬퍼
|
||||
// =========================================================================
|
||||
|
||||
private void reload() throws Exception {
|
||||
clearField("configMap");
|
||||
clearField("dynamicKeyCache");
|
||||
clearField("strategyCache");
|
||||
|
||||
Method loadAll = CryptoModuleManager.class.getDeclaredMethod("loadAll");
|
||||
loadAll.setAccessible(true);
|
||||
loadAll.invoke(manager);
|
||||
|
||||
reset(mockHsmCryptoService);
|
||||
SecretKey masterKey = new SecretKeySpec(KEY_128, "AES");
|
||||
when(mockHsmCryptoService.getSecretKey(anyString())).thenReturn(masterKey);
|
||||
}
|
||||
|
||||
private void clearField(String fieldName) throws Exception {
|
||||
Field field = CryptoModuleManager.class.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
((Map<?, ?>) field.get(manager)).clear();
|
||||
}
|
||||
|
||||
private CryptoModuleConfig buildStaticConfig(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(java.util.UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("STATIC");
|
||||
c.setEncKeyHex(ENC_KEY_HEX);
|
||||
c.setDecKeyHex(ENC_KEY_HEX);
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private CryptoModuleConfig buildDynamicConfig(String name, String alg, String mode, String padding) {
|
||||
CryptoModuleConfig c = new CryptoModuleConfig();
|
||||
c.setCryptoId(java.util.UUID.randomUUID().toString());
|
||||
c.setCryptoName(name);
|
||||
c.setAlgType(alg);
|
||||
c.setCipherMode(mode);
|
||||
c.setPadding(padding);
|
||||
c.setIvHex(IV_HEX);
|
||||
c.setKeySourceType("DYNAMIC");
|
||||
c.setKeyDerivStrategy("com.eactive.eai.common.security.keyderiv.strategy.HsmContextXorKeyDerivationStrategy");
|
||||
c.setKeyDerivParams("{\"hsmKeyAlias\":\"MASTER_KEY\",\"contextKey\":\"X-Api-Enc-Key\",\"offset\":\"0\",\"length\":\"16\"}");
|
||||
c.setCacheYn("Y");
|
||||
c.setCacheTtlSec(300);
|
||||
c.setUseYn("Y");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) sb.append(String.format("%02x", b));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** MapStruct APT 없이도 동작하는 인라인 매퍼 구현체 */
|
||||
private static CryptoModuleConfigMapper testMapper() {
|
||||
return new CryptoModuleConfigMapper() {
|
||||
@Override
|
||||
public CryptoModuleConfigVO toVo(CryptoModuleConfig e) {
|
||||
CryptoModuleConfigVO vo = new CryptoModuleConfigVO();
|
||||
vo.setCryptoId(e.getCryptoId());
|
||||
vo.setCryptoName(e.getCryptoName());
|
||||
vo.setCryptoDesc(e.getCryptoDesc());
|
||||
vo.setAlgType(e.getAlgType());
|
||||
vo.setCipherMode(e.getCipherMode());
|
||||
vo.setPadding(e.getPadding());
|
||||
vo.setIvHex(e.getIvHex());
|
||||
vo.setKeySourceType(e.getKeySourceType());
|
||||
vo.setEncKeyHex(e.getEncKeyHex());
|
||||
vo.setDecKeyHex(e.getDecKeyHex());
|
||||
vo.setKeyDerivStrategy(e.getKeyDerivStrategy());
|
||||
vo.setKeyDerivParams(e.getKeyDerivParams());
|
||||
vo.setCacheYn(e.getCacheYn());
|
||||
vo.setCacheTtlSec(e.getCacheTtlSec());
|
||||
vo.setUseYn(e.getUseYn());
|
||||
return vo;
|
||||
}
|
||||
@Override
|
||||
public CryptoModuleConfig toEntity(CryptoModuleConfigVO vo) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.eactive.eai.manage.session;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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;
|
||||
|
||||
import com.eactive.eai.common.session.CacheInfoVO;
|
||||
|
||||
/**
|
||||
* SessionCacheManageController 단위 테스트.
|
||||
*
|
||||
* <p>Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
|
||||
* 응답 구조(success, data, count, message)를 검증한다.
|
||||
*/
|
||||
class SessionCacheManageControllerTest {
|
||||
|
||||
private SessionCacheManageController controller;
|
||||
private SessionCacheManageService mockService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new SessionCacheManageController();
|
||||
mockService = mock(SessionCacheManageService.class);
|
||||
ReflectionTestUtils.setField(controller, "sessionCacheManageService", mockService);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helper
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private CacheInfoVO makeCacheInfo(String name) {
|
||||
CacheInfoVO vo = new CacheInfoVO();
|
||||
vo.setName(name);
|
||||
vo.setSize(3);
|
||||
vo.setKeys(Arrays.asList("a", "b", "c"));
|
||||
vo.setTimeToLive(60000);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> body(ResponseEntity<?> response) {
|
||||
return (Map<String, Object>) response.getBody();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 캐시 요약 / 마스터 인스턴스
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("getCacheStatus() — success=true, data에 원문 그대로 반환")
|
||||
void getCacheStatus_returnsRawStatusString() {
|
||||
when(mockService.getCacheStatus()).thenReturn("<Caches type=\"Ignite\"></Caches>");
|
||||
|
||||
Map<String, Object> body = body(controller.getCacheStatus());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertEquals("<Caches type=\"Ignite\"></Caches>", body.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getMasterInstId() — success=true, data에 인스턴스명 반환")
|
||||
void getMasterInstId_returnsInstanceName() {
|
||||
when(mockService.getMasterInstId()).thenReturn("SERVER01");
|
||||
|
||||
Map<String, Object> body = body(controller.getMasterInstId());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertEquals("SERVER01", body.get("data"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 개별 캐시 조회
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("getLoginCache() — success=true, CacheInfoVO 반환")
|
||||
void getLoginCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("LoginCache");
|
||||
when(mockService.getLoginCache()).thenReturn(vo);
|
||||
|
||||
ResponseEntity<?> response = controller.getLoginCache();
|
||||
Map<String, Object> body = body(response);
|
||||
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(vo, body.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getHttpLoginCache() — success=true")
|
||||
void getHttpLoginCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("HttpLoginCache");
|
||||
when(mockService.getHttpLoginCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, body(controller.getHttpLoginCache()).get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getEvictMasterCache() — success=true")
|
||||
void getEvictMasterCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("EvictMasterCache");
|
||||
when(mockService.getEvictMasterCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, body(controller.getEvictMasterCache()).get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getTerminalCache() — success=true")
|
||||
void getTerminalCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("TerminalCache");
|
||||
when(mockService.getTerminalCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, body(controller.getTerminalCache()).get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getConvertUserIdCache() — success=true")
|
||||
void getConvertUserIdCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache");
|
||||
when(mockService.getConvertUserIdCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, body(controller.getConvertUserIdCache()).get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getSocketCache() — success=true")
|
||||
void getSocketCache_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("SocketCache");
|
||||
when(mockService.getSocketCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, body(controller.getSocketCache()).get("data"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 전체 캐시 목록
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("getAllCaches() — 목록과 개수 반환")
|
||||
void getAllCaches_returnsListAndCount() {
|
||||
List<CacheInfoVO> list = Arrays.asList(makeCacheInfo("A"), makeCacheInfo("B"));
|
||||
when(mockService.getAllCaches()).thenReturn(list);
|
||||
|
||||
Map<String, Object> body = body(controller.getAllCaches());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(list, body.get("data"));
|
||||
assertEquals(2, body.get("count"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getAllCaches() — 빈 목록이면 count=0")
|
||||
void getAllCaches_emptyList_countZero() {
|
||||
when(mockService.getAllCaches()).thenReturn(Arrays.asList());
|
||||
|
||||
Map<String, Object> body = body(controller.getAllCaches());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertEquals(0, body.get("count"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// OutboundAccessToken 캐시 — 지원/미지원 백엔드
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("getOutboundAccessTokenCache() — 지원 시 success=true, CacheInfoVO 반환")
|
||||
void getOutboundAccessTokenCache_supported_returnsCacheInfo() {
|
||||
CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken");
|
||||
when(mockService.getOutboundAccessTokenCache()).thenReturn(vo);
|
||||
|
||||
Map<String, Object> body = body(controller.getOutboundAccessTokenCache());
|
||||
|
||||
assertEquals(Boolean.TRUE, body.get("success"));
|
||||
assertSame(vo, body.get("data"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("getOutboundAccessTokenCache() — Ehcache 등 미지원 백엔드는 500이 아니라 success=false로 응답")
|
||||
void getOutboundAccessTokenCache_unsupported_successFalseNotServerError() {
|
||||
when(mockService.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
|
||||
|
||||
ResponseEntity<?> response = controller.getOutboundAccessTokenCache();
|
||||
Map<String, Object> body = body(response);
|
||||
|
||||
assertEquals(200, response.getStatusCodeValue());
|
||||
assertEquals(Boolean.FALSE, body.get("success"));
|
||||
assertNotNull(body.get("message"));
|
||||
assertFalse(body.containsKey("data"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.eactive.eai.manage.session;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.eactive.eai.common.session.CacheInfoVO;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
|
||||
/**
|
||||
* SessionCacheManageService 단위 테스트.
|
||||
*
|
||||
* <p>SessionManager.getInstance()는 static singleton(private static instance 필드)이라
|
||||
* 실제 Ignite/Ehcache를 기동하지 않고, 리플렉션으로 그 필드를 Mockito mock으로 교체하여 검증한다.
|
||||
* 테스트가 끝나면 반드시 필드를 원복(null)하여 다른 테스트에 영향을 주지 않는다.
|
||||
*/
|
||||
class SessionCacheManageServiceTest {
|
||||
|
||||
private SessionManager mockSessionManager;
|
||||
private final SessionCacheManageService service = new SessionCacheManageService();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
mockSessionManager = mock(SessionManager.class);
|
||||
setInstance(mockSessionManager);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
setInstance(null);
|
||||
}
|
||||
|
||||
private void setInstance(SessionManager manager) throws Exception {
|
||||
Field field = SessionManager.class.getDeclaredField("instance");
|
||||
field.setAccessible(true);
|
||||
field.set(null, manager);
|
||||
}
|
||||
|
||||
private CacheInfoVO makeCacheInfo(String name, int size) {
|
||||
CacheInfoVO vo = new CacheInfoVO();
|
||||
vo.setName(name);
|
||||
vo.setSize(size);
|
||||
vo.setKeys(Arrays.asList("k1", "k2"));
|
||||
vo.setTimeToLive(60000);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 1. 단건 위임
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("1-1. getCacheStatus() — SessionManager.getCacheStatus() 위임")
|
||||
void testGetCacheStatus_delegates() {
|
||||
when(mockSessionManager.getCacheStatus()).thenReturn("<Caches type=\"Ignite\"></Caches>");
|
||||
|
||||
assertEquals("<Caches type=\"Ignite\"></Caches>", service.getCacheStatus());
|
||||
verify(mockSessionManager).getCacheStatus();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-2. getMasterInstId() — SessionManager.getMasterInstId() 위임")
|
||||
void testGetMasterInstId_delegates() {
|
||||
when(mockSessionManager.getMasterInstId()).thenReturn("SERVER01");
|
||||
|
||||
assertEquals("SERVER01", service.getMasterInstId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-3. getLoginCache() — 위임")
|
||||
void testGetLoginCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("LoginCache", 2);
|
||||
when(mockSessionManager.getLoginCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getLoginCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-4. getHttpLoginCache() — 위임")
|
||||
void testGetHttpLoginCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("HttpLoginCache", 1);
|
||||
when(mockSessionManager.getHttpLoginCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getHttpLoginCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-5. getEvictMasterCache() — 위임")
|
||||
void testGetEvictMasterCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("EvictMasterCache", 1);
|
||||
when(mockSessionManager.getEvictMasterCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getEvictMasterCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-6. getTerminalCache() — 위임")
|
||||
void testGetTerminalCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("TerminalCache", 5);
|
||||
when(mockSessionManager.getTerminalCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getTerminalCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-7. getConvertUserIdCache() — 위임")
|
||||
void testGetConvertUserIdCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("ConvertUserIdCache", 3);
|
||||
when(mockSessionManager.getConvertUserIdCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getConvertUserIdCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-8. getSocketCache() — 위임")
|
||||
void testGetSocketCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("SocketCache", 4);
|
||||
when(mockSessionManager.getSocketCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getSocketCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-9. getOutboundAccessTokenCache() — 위임")
|
||||
void testGetOutboundAccessTokenCache_delegates() {
|
||||
CacheInfoVO vo = makeCacheInfo("OutBoundAccessToken", 7);
|
||||
when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(vo);
|
||||
|
||||
assertSame(vo, service.getOutboundAccessTokenCache());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("1-10. getOutboundAccessTokenCache() — 미지원 백엔드는 예외 그대로 전파")
|
||||
void testGetOutboundAccessTokenCache_unsupported_throwsException() {
|
||||
when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
|
||||
|
||||
assertThrows(UnsupportedOperationException.class, service::getOutboundAccessTokenCache);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 2. getAllCaches() 집계
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
@DisplayName("2-1. getAllCaches() — OutboundAccessToken 포함 7개 캐시 반환")
|
||||
void testGetAllCaches_outboundSupported_returnsSevenCaches() {
|
||||
when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1));
|
||||
when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1));
|
||||
when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1));
|
||||
when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1));
|
||||
when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1));
|
||||
when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1));
|
||||
when(mockSessionManager.getOutboundAccessTokenCache()).thenReturn(makeCacheInfo("OutBoundAccessToken", 1));
|
||||
|
||||
List<CacheInfoVO> all = service.getAllCaches();
|
||||
|
||||
assertEquals(7, all.size());
|
||||
assertTrue(all.stream().anyMatch(c -> "OutBoundAccessToken".equals(c.getName())));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2-2. getAllCaches() — OutboundAccessToken 미지원(Ehcache)이면 예외 없이 6개만 반환")
|
||||
void testGetAllCaches_outboundUnsupported_excludedGracefully() {
|
||||
when(mockSessionManager.getLoginCache()).thenReturn(makeCacheInfo("LoginCache", 1));
|
||||
when(mockSessionManager.getHttpLoginCache()).thenReturn(makeCacheInfo("HttpLoginCache", 1));
|
||||
when(mockSessionManager.getEvictMasterCache()).thenReturn(makeCacheInfo("EvictMasterCache", 1));
|
||||
when(mockSessionManager.getTerminalCache()).thenReturn(makeCacheInfo("TerminalCache", 1));
|
||||
when(mockSessionManager.getConvertUserIdCache()).thenReturn(makeCacheInfo("ConvertUserIdCache", 1));
|
||||
when(mockSessionManager.getSocketCache()).thenReturn(makeCacheInfo("SocketCache", 1));
|
||||
when(mockSessionManager.getOutboundAccessTokenCache()).thenThrow(new UnsupportedOperationException());
|
||||
|
||||
List<CacheInfoVO> all = assertDoesNotThrow(service::getAllCaches);
|
||||
|
||||
assertEquals(6, all.size());
|
||||
assertTrue(all.stream().noneMatch(c -> "OutBoundAccessToken".equals(c.getName())));
|
||||
}
|
||||
}
|
||||
@@ -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