ToolsController 개발

- base64 인코딩, damo 암복호화, version 정보
This commit is contained in:
curry772
2026-07-23 11:43:59 +09:00
parent cb0a588a9c
commit 9e1f7391a0
8 changed files with 901 additions and 0 deletions
@@ -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;
}
@@ -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());
}
}