From 2eeee58d35938d5ffdccd0df141cf3b1830484e2 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 15:15:18 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EB=B2=84=ED=82=B7=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20Service/Controller=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getDualManager()를 protected로 변경하여 서브클래스 오버라이드 방식으로 mock 주입 Co-Authored-By: Claude Sonnet 4.6 --- elink-online-common | 2 +- .../inflow/InflowTargetBucketService.java | 2 +- .../InflowTargetBucketControllerTest.java | 211 ++++++++++++ .../inflow/InflowTargetBucketServiceTest.java | 315 ++++++++++++++++++ 4 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketControllerTest.java create mode 100644 src/test/java/com/eactive/eai/manage/inflow/InflowTargetBucketServiceTest.java 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()); + } +}