diff --git a/src/main/java/com/eactive/eai/manage/tools/EncryptionManagerStatusDTO.java b/src/main/java/com/eactive/eai/manage/tools/EncryptionManagerStatusDTO.java new file mode 100644 index 0000000..6ff0e40 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/tools/EncryptionManagerStatusDTO.java @@ -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; +} diff --git a/src/main/java/com/eactive/eai/manage/tools/ToolsController.java b/src/main/java/com/eactive/eai/manage/tools/ToolsController.java new file mode 100644 index 0000000..128ac7a --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/tools/ToolsController.java @@ -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 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 action) { + Map 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); + } +} diff --git a/src/main/java/com/eactive/eai/manage/tools/ToolsService.java b/src/main/java/com/eactive/eai/manage/tools/ToolsService.java new file mode 100644 index 0000000..74e8662 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/tools/ToolsService.java @@ -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.<서브모듈명>= 형태의 키를 모아 서브모듈별 버전으로 담는다. + Map 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; + } +} diff --git a/src/main/java/com/eactive/eai/manage/tools/ToolsTextRequestDTO.java b/src/main/java/com/eactive/eai/manage/tools/ToolsTextRequestDTO.java new file mode 100644 index 0000000..186214b --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/tools/ToolsTextRequestDTO.java @@ -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; +} diff --git a/src/main/java/com/eactive/eai/manage/tools/VersionInfoDTO.java b/src/main/java/com/eactive/eai/manage/tools/VersionInfoDTO.java new file mode 100644 index 0000000..ac1512b --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/tools/VersionInfoDTO.java @@ -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 moduleVersions; +} diff --git a/src/test/java/com/eactive/eai/manage/tools/ToolsControllerTest.java b/src/test/java/com/eactive/eai/manage/tools/ToolsControllerTest.java new file mode 100644 index 0000000..1da2089 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/tools/ToolsControllerTest.java @@ -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 단위 테스트. + * + *

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 body(ResponseEntity response) { + return (Map) 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 body = body(controller.getVersionInfo()); + + assertEquals(Boolean.FALSE, body.get("success")); + assertEquals("version.info 읽기 실패", body.get("message")); + } +} diff --git a/src/test/java/com/eactive/eai/manage/tools/ToolsServiceEncryptionManagerTest.java b/src/test/java/com/eactive/eai/manage/tools/ToolsServiceEncryptionManagerTest.java new file mode 100644 index 0000000..fedcea7 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/tools/ToolsServiceEncryptionManagerTest.java @@ -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) 단위 테스트. + * + *

EncryptionManager.getInstance()는 ApplicationContextProvider를 통해 실제 Spring + * ApplicationContext에서 빈을 조회하므로, base64/hex/damo 단독 테스트(ToolsServiceTest)와 분리해 + * 최소한의 GenericApplicationContext를 구성한다. + * + *

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 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()); + } +} diff --git a/src/test/java/com/eactive/eai/manage/tools/ToolsServiceTest.java b/src/test/java/com/eactive/eai/manage/tools/ToolsServiceTest.java new file mode 100644 index 0000000..708bcc5 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/tools/ToolsServiceTest.java @@ -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 단위 테스트. + * + *

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()); + } +}