diff --git a/elink-online-common b/elink-online-common
index d08c7ef..9589bc6 160000
--- a/elink-online-common
+++ b/elink-online-common
@@ -1 +1 @@
-Subproject commit d08c7efcdcd8e20f210ff6bdd025283d07606955
+Subproject commit 9589bc635aac190ddea60f065fe397231fbfcd7b
diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java
index 1ec9a3a..6168aae 100644
--- a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java
+++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java
@@ -78,7 +78,7 @@ public class InflowTargetBucketService {
// Private helpers
// -------------------------------------------------------------------------
- private DualInflowControlManager getDualManager() {
+ protected DualInflowControlManager getDualManager() {
InflowControlManager base = InflowControlManager.getInstance();
return (base instanceof DualInflowControlManager) ? (DualInflowControlManager) base : null;
}
diff --git a/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketControllerTest.java b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketControllerTest.java
new file mode 100644
index 0000000..e028725
--- /dev/null
+++ b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketControllerTest.java
@@ -0,0 +1,211 @@
+package com.eactive.eai.manage.inflow;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+
+/**
+ * InflowTargetBucketController 단위 테스트.
+ *
+ *
Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여
+ * 응답 구조(success, data, count, message)를 검증한다.
+ */
+class InflowTargetBucketControllerTest {
+
+ private InflowTargetBucketController controller;
+ private InflowTargetBucketService mockService;
+
+ @BeforeEach
+ void setUp() {
+ controller = new InflowTargetBucketController();
+ mockService = mock(InflowTargetBucketService.class);
+ ReflectionTestUtils.setField(controller, "bucketService", mockService);
+ }
+
+ // -------------------------------------------------------------------------
+ // Helper
+ // -------------------------------------------------------------------------
+
+ private TargetBucketStatusDTO makeDTO(String id, String type) {
+ TargetBucketStatusDTO dto = new TargetBucketStatusDTO();
+ dto.setTargetId(id);
+ dto.setTargetType(type);
+ dto.setActivate(true);
+ dto.setBuckets(Collections.emptyList());
+ return dto;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map body(ResponseEntity> response) {
+ return (Map) response.getBody();
+ }
+
+ // =========================================================================
+ // 어댑터 — 단일 조회
+ // =========================================================================
+
+ @Test
+ void getAdapterBucketStatus_found_successTrueAndDataPresent() {
+ TargetBucketStatusDTO dto = makeDTO("ADAPTER_A", "ADAPTER");
+ when(mockService.getAdapterBucketStatus("ADAPTER_A")).thenReturn(dto);
+
+ ResponseEntity> response = controller.getAdapterBucketStatus("ADAPTER_A");
+ Map body = body(response);
+
+ assertEquals(200, response.getStatusCodeValue());
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertSame(dto, body.get("data"));
+ assertFalse(body.containsKey("message"));
+ }
+
+ @Test
+ void getAdapterBucketStatus_notFound_successFalseWithAdapterId() {
+ when(mockService.getAdapterBucketStatus("GHOST")).thenReturn(null);
+
+ ResponseEntity> response = controller.getAdapterBucketStatus("GHOST");
+ Map body = body(response);
+
+ assertEquals(Boolean.FALSE, body.get("success"));
+ assertTrue(body.get("message").toString().contains("GHOST"));
+ }
+
+ // =========================================================================
+ // 어댑터 — 전체 조회
+ // =========================================================================
+
+ @Test
+ void getAllAdapterBucketStatus_returnsListAndCount() {
+ List list = Arrays.asList(
+ makeDTO("A1", "ADAPTER"),
+ makeDTO("A2", "ADAPTER")
+ );
+ when(mockService.getAllAdapterBucketStatus()).thenReturn(list);
+
+ ResponseEntity> response = controller.getAllAdapterBucketStatus();
+ Map body = body(response);
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertSame(list, body.get("data"));
+ assertEquals(2, body.get("count"));
+ }
+
+ @Test
+ void getAllAdapterBucketStatus_emptyList_countZero() {
+ when(mockService.getAllAdapterBucketStatus()).thenReturn(Collections.emptyList());
+
+ Map body = body(controller.getAllAdapterBucketStatus());
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertEquals(0, body.get("count"));
+ }
+
+ // =========================================================================
+ // 인터페이스 — 단일 조회
+ // =========================================================================
+
+ @Test
+ void getInterfaceBucketStatus_found_successTrue() {
+ TargetBucketStatusDTO dto = makeDTO("IF_001", "INTERFACE");
+ when(mockService.getInterfaceBucketStatus("IF_001")).thenReturn(dto);
+
+ ResponseEntity> response = controller.getInterfaceBucketStatus("IF_001");
+ Map body = body(response);
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertSame(dto, body.get("data"));
+ }
+
+ @Test
+ void getInterfaceBucketStatus_notFound_successFalseWithInterfaceId() {
+ when(mockService.getInterfaceBucketStatus("GHOST")).thenReturn(null);
+
+ Map body = body(controller.getInterfaceBucketStatus("GHOST"));
+
+ assertEquals(Boolean.FALSE, body.get("success"));
+ assertTrue(body.get("message").toString().contains("GHOST"));
+ }
+
+ // =========================================================================
+ // 인터페이스 — 전체 조회
+ // =========================================================================
+
+ @Test
+ void getAllInterfaceBucketStatus_returnsListAndCount() {
+ List list = Collections.singletonList(makeDTO("IF_001", "INTERFACE"));
+ when(mockService.getAllInterfaceBucketStatus()).thenReturn(list);
+
+ Map body = body(controller.getAllInterfaceBucketStatus());
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertEquals(1, body.get("count"));
+ }
+
+ // =========================================================================
+ // 클라이언트 — 단일 조회
+ // =========================================================================
+
+ @Test
+ void getClientBucketStatus_found_successTrueAndDataPresent() {
+ TargetBucketStatusDTO dto = makeDTO("CLIENT_A", "CLIENT");
+ when(mockService.getClientBucketStatus("CLIENT_A")).thenReturn(dto);
+
+ ResponseEntity> response = controller.getClientBucketStatus("CLIENT_A");
+ Map body = body(response);
+
+ assertEquals(200, response.getStatusCodeValue());
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertSame(dto, body.get("data"));
+ assertFalse(body.containsKey("message"));
+ }
+
+ @Test
+ void getClientBucketStatus_notFound_successFalseWithClientId() {
+ when(mockService.getClientBucketStatus("CLIENT_X")).thenReturn(null);
+
+ ResponseEntity> response = controller.getClientBucketStatus("CLIENT_X");
+ Map body = body(response);
+
+ assertEquals(Boolean.FALSE, body.get("success"));
+ assertTrue(body.get("message").toString().contains("CLIENT_X"));
+ }
+
+ // =========================================================================
+ // 클라이언트 — 전체 조회
+ // =========================================================================
+
+ @Test
+ void getAllClientBucketStatus_returnsListAndCount() {
+ List list = Arrays.asList(
+ makeDTO("CLIENT_A", "CLIENT"),
+ makeDTO("CLIENT_B", "CLIENT"),
+ makeDTO("CLIENT_C", "CLIENT")
+ );
+ when(mockService.getAllClientBucketStatus()).thenReturn(list);
+
+ ResponseEntity> response = controller.getAllClientBucketStatus();
+ Map body = body(response);
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertSame(list, body.get("data"));
+ assertEquals(3, body.get("count"));
+ }
+
+ @Test
+ void getAllClientBucketStatus_emptyList_countZero() {
+ when(mockService.getAllClientBucketStatus()).thenReturn(Collections.emptyList());
+
+ Map body = body(controller.getAllClientBucketStatus());
+
+ assertEquals(Boolean.TRUE, body.get("success"));
+ assertEquals(0, body.get("count"));
+ }
+}
diff --git a/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketServiceTest.java b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketServiceTest.java
new file mode 100644
index 0000000..2037ced
--- /dev/null
+++ b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketServiceTest.java
@@ -0,0 +1,315 @@
+package com.eactive.eai.manage.inflow;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.junit.jupiter.api.Test;
+
+import com.eactive.eai.common.inflow.InflowTargetVO;
+import com.eactive.eai.common.inflow.dual.DualCustomBucket;
+import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
+
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket4j;
+import io.github.bucket4j.local.LocalBucket;
+
+/**
+ * InflowTargetBucketService 단위 테스트.
+ *
+ * getDualManager()를 protected로 오버라이드하는 내부 테스트 서브클래스를 사용하여
+ * DualInflowControlManager 의존성을 주입한다.
+ */
+class InflowTargetBucketServiceTest {
+
+ // -------------------------------------------------------------------------
+ // 테스트용 서브클래스
+ // -------------------------------------------------------------------------
+
+ private static InflowTargetBucketService serviceWith(DualInflowControlManager manager) {
+ return new InflowTargetBucketService() {
+ @Override
+ protected DualInflowControlManager getDualManager() {
+ return manager;
+ }
+ };
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit, boolean activate) {
+ InflowTargetVO vo = new InflowTargetVO();
+ vo.setName(name);
+ vo.setThresholdPerSecond(perSec);
+ vo.setThreshold(threshold);
+ vo.setThresholdTimeUnit(unit);
+ vo.setActivate(activate);
+ return vo;
+ }
+
+ private LocalBucket freshBucket(long capacity) {
+ return Bucket4j.builder()
+ .addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(1)))
+ .build();
+ }
+
+ private LocalBucket exhaustedBucket(long capacity) {
+ LocalBucket b = freshBucket(capacity);
+ b.tryConsume(capacity);
+ return b;
+ }
+
+ private DualCustomBucket makeDualBucket(String name, long perSec, long threshold) {
+ return new DualCustomBucket(freshBucket(perSec), freshBucket(threshold),
+ makeVO(name, perSec, threshold, "DAY", true));
+ }
+
+ // =========================================================================
+ // DualManager null (비활성 환경)
+ // =========================================================================
+
+ @Test
+ void getAdapterBucketStatus_managerIsNull_returnsNull() {
+ assertNull(serviceWith(null).getAdapterBucketStatus("ADAPTER_A"));
+ }
+
+ @Test
+ void getAllAdapterBucketStatus_managerIsNull_returnsEmptyList() {
+ List result = serviceWith(null).getAllAdapterBucketStatus();
+ assertNotNull(result);
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void getInterfaceBucketStatus_managerIsNull_returnsNull() {
+ assertNull(serviceWith(null).getInterfaceBucketStatus("IF_001"));
+ }
+
+ @Test
+ void getAllInterfaceBucketStatus_managerIsNull_returnsEmptyList() {
+ assertTrue(serviceWith(null).getAllInterfaceBucketStatus().isEmpty());
+ }
+
+ @Test
+ void getClientBucketStatus_managerIsNull_returnsNull() {
+ assertNull(serviceWith(null).getClientBucketStatus("CLIENT_A"));
+ }
+
+ @Test
+ void getAllClientBucketStatus_managerIsNull_returnsEmptyList() {
+ assertTrue(serviceWith(null).getAllClientBucketStatus().isEmpty());
+ }
+
+ // =========================================================================
+ // 버킷 미존재
+ // =========================================================================
+
+ @Test
+ void getAdapterBucketStatus_notFound_returnsNull() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getAdapterBucket("ADAPTER_A")).thenReturn(null);
+
+ assertNull(serviceWith(manager).getAdapterBucketStatus("ADAPTER_A"));
+ }
+
+ @Test
+ void getInterfaceBucketStatus_notFound_returnsNull() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getInterfaceBucket("IF_001")).thenReturn(null);
+
+ assertNull(serviceWith(manager).getInterfaceBucketStatus("IF_001"));
+ }
+
+ @Test
+ void getClientBucketStatus_notFound_returnsNull() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("CLIENT_A")).thenReturn(null);
+
+ assertNull(serviceWith(manager).getClientBucketStatus("CLIENT_A"));
+ }
+
+ // =========================================================================
+ // 단일 조회 — targetType, targetId 매핑
+ // =========================================================================
+
+ @Test
+ void getAdapterBucketStatus_found_mapsAdapterType() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getAdapterBucket("ADAPTER_A")).thenReturn(makeDualBucket("ADAPTER_A", 10, 1000));
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getAdapterBucketStatus("ADAPTER_A");
+
+ assertNotNull(dto);
+ assertEquals("ADAPTER_A", dto.getTargetId());
+ assertEquals("ADAPTER", dto.getTargetType());
+ assertTrue(dto.isActivate());
+ assertEquals(2, dto.getBuckets().size());
+ }
+
+ @Test
+ void getInterfaceBucketStatus_found_mapsInterfaceType() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getInterfaceBucket("IF_001")).thenReturn(makeDualBucket("IF_001", 20, 2000));
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getInterfaceBucketStatus("IF_001");
+
+ assertNotNull(dto);
+ assertEquals("IF_001", dto.getTargetId());
+ assertEquals("INTERFACE", dto.getTargetType());
+ assertTrue(dto.isActivate());
+ assertEquals(2, dto.getBuckets().size());
+ }
+
+ @Test
+ void getClientBucketStatus_found_mapsClientType() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("CLIENT_A")).thenReturn(makeDualBucket("CLIENT_A", 50, 5000));
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("CLIENT_A");
+
+ assertNotNull(dto);
+ assertEquals("CLIENT_A", dto.getTargetId());
+ assertEquals("CLIENT", dto.getTargetType());
+ assertTrue(dto.isActivate());
+ assertEquals(2, dto.getBuckets().size());
+ }
+
+ // =========================================================================
+ // BucketInfo 타입별 포함 여부
+ // =========================================================================
+
+ @Test
+ void toDto_onlyThreshold_bucketInfoHasOneThresholdEntry() {
+ InflowTargetVO vo = makeVO("C1", 0, 1000, "DAY", true);
+ DualCustomBucket bucket = new DualCustomBucket(null, freshBucket(1000), vo);
+
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("C1")).thenReturn(bucket);
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("C1");
+
+ assertEquals(1, dto.getBuckets().size());
+ assertEquals("threshold", dto.getBuckets().get(0).getType());
+ }
+
+ @Test
+ void toDto_onlyPerSecond_bucketInfoHasOnePerSecondEntry() {
+ InflowTargetVO vo = makeVO("C2", 100, 0, "SEC", true);
+ DualCustomBucket bucket = new DualCustomBucket(freshBucket(100), null, vo);
+
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("C2")).thenReturn(bucket);
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("C2");
+
+ assertEquals(1, dto.getBuckets().size());
+ assertEquals("perSecond", dto.getBuckets().get(0).getType());
+ }
+
+ @Test
+ void toDto_bothBuckets_bucketInfoHasTwoEntries() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("C3")).thenReturn(makeDualBucket("C3", 100, 5000));
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("C3");
+
+ assertEquals(2, dto.getBuckets().size());
+ assertEquals("perSecond", dto.getBuckets().get(0).getType());
+ assertEquals("threshold", dto.getBuckets().get(1).getType());
+ }
+
+ // =========================================================================
+ // 사용률 계산
+ // =========================================================================
+
+ @Test
+ void toDto_exhaustedThreshold_usagePercentIs100() {
+ InflowTargetVO vo = makeVO("C4", 0, 100, "DAY", true);
+ DualCustomBucket bucket = new DualCustomBucket(null, exhaustedBucket(100), vo);
+
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("C4")).thenReturn(bucket);
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("C4");
+
+ GroupBucketStatusDTO.BucketInfo info = dto.getBuckets().get(0);
+ assertEquals(0, info.getAvailableTokens());
+ assertEquals(100.0, info.getUsagePercent(), 0.01);
+ }
+
+ @Test
+ void toDto_freshBucket_usagePercentIsZero() {
+ InflowTargetVO vo = makeVO("C5", 0, 100, "DAY", true);
+ DualCustomBucket bucket = new DualCustomBucket(null, freshBucket(100), vo);
+
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getClientBucket("C5")).thenReturn(bucket);
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getClientBucketStatus("C5");
+
+ GroupBucketStatusDTO.BucketInfo info = dto.getBuckets().get(0);
+ assertEquals(100, info.getAvailableTokens());
+ assertEquals(0.0, info.getUsagePercent(), 0.01);
+ }
+
+ @Test
+ void toDto_capacity_perSecondBucketInfoCapacityMatchesVO() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getAdapterBucket("A1")).thenReturn(makeDualBucket("A1", 30, 3000));
+
+ TargetBucketStatusDTO dto = serviceWith(manager).getAdapterBucketStatus("A1");
+
+ GroupBucketStatusDTO.BucketInfo ps = dto.getBuckets().get(0);
+ assertEquals(30, ps.getCapacity());
+ assertEquals("SEC", ps.getTimeUnit());
+
+ GroupBucketStatusDTO.BucketInfo th = dto.getBuckets().get(1);
+ assertEquals(3000, th.getCapacity());
+ assertEquals("DAY", th.getTimeUnit());
+ }
+
+ // =========================================================================
+ // 전체 목록 조회
+ // =========================================================================
+
+ @Test
+ void getAllClientBucketStatus_twoEntries_returnsBoth() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ Map map = new ConcurrentHashMap<>();
+ map.put("CLIENT_A", makeDualBucket("CLIENT_A", 10, 1000));
+ map.put("CLIENT_B", makeDualBucket("CLIENT_B", 20, 2000));
+ when(manager.getClientBucketMap()).thenReturn(map);
+
+ List result = serviceWith(manager).getAllClientBucketStatus();
+
+ assertEquals(2, result.size());
+ }
+
+ @Test
+ void getAllAdapterBucketStatus_emptyMap_returnsEmptyList() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ when(manager.getAdapterBucketMap()).thenReturn(new ConcurrentHashMap<>());
+
+ assertTrue(serviceWith(manager).getAllAdapterBucketStatus().isEmpty());
+ }
+
+ @Test
+ void getAllInterfaceBucketStatus_oneEntry_targetTypeIsInterface() {
+ DualInflowControlManager manager = mock(DualInflowControlManager.class);
+ Map map = new ConcurrentHashMap<>();
+ map.put("IF_001", makeDualBucket("IF_001", 10, 1000));
+ when(manager.getInterfaceBucketMap()).thenReturn(map);
+
+ List result = serviceWith(manager).getAllInterfaceBucketStatus();
+
+ assertEquals(1, result.size());
+ assertEquals("INTERFACE", result.get(0).getTargetType());
+ assertEquals("IF_001", result.get(0).getTargetId());
+ }
+}