From 5f24bacd48536a990474c1c6261a1669bb49e530 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 14:59:26 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=96=B4=EB=8C=91=ED=84=B0/?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4/=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EB=B2=84=ED=82=B7=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InflowTargetBucketController: 버킷 상태 조회 REST API GET /manage/inflow/adapter/bucket-status GET /manage/inflow/adapter/{adapterId}/bucket-status GET /manage/inflow/interface/bucket-status GET /manage/inflow/interface/{interfaceId}/bucket-status GET /manage/inflow/client/bucket-status GET /manage/inflow/client/{clientId}/bucket-status - InflowTargetBucketService: DualInflowControlManager 에서 버킷 상태 조회 - TargetBucketStatusDTO: 어댑터/인터페이스/클라이언트 공통 버킷 상태 DTO Co-Authored-By: Claude Sonnet 4.6 --- .../inflow/InflowTargetBucketController.java | 106 +++++++++++++++ .../inflow/InflowTargetBucketService.java | 121 ++++++++++++++++++ .../manage/inflow/TargetBucketStatusDTO.java | 27 ++++ 3 files changed, 254 insertions(+) create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketController.java create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java create mode 100644 src/main/java/com/eactive/eai/manage/inflow/TargetBucketStatusDTO.java diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketController.java b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketController.java new file mode 100644 index 0000000..1d26c14 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketController.java @@ -0,0 +1,106 @@ +package com.eactive.eai.manage.inflow; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 어댑터/인터페이스/클라이언트 버킷 상태 모니터링 API. + * + * DualInflowControlManager 가 활성화된 환경에서만 정상 응답한다. + * 비활성화 환경에서는 success=false 또는 빈 목록을 반환한다. + * + * GET /manage/inflow/adapter/bucket-status → 전체 어댑터 버킷 상태 + * GET /manage/inflow/adapter/{adapterId}/bucket-status → 특정 어댑터 버킷 상태 + * GET /manage/inflow/interface/bucket-status → 전체 인터페이스 버킷 상태 + * GET /manage/inflow/interface/{interfaceId}/bucket-status → 특정 인터페이스 버킷 상태 + * GET /manage/inflow/client/bucket-status → 전체 클라이언트 버킷 상태 + * GET /manage/inflow/client/{clientId}/bucket-status → 특정 클라이언트 버킷 상태 + */ +@RestController +@RequestMapping("/manage/inflow") +public class InflowTargetBucketController { + + @Autowired + private InflowTargetBucketService bucketService; + + // ========================================================================= + // 어댑터 + // ========================================================================= + + @GetMapping("/adapter/bucket-status") + public ResponseEntity getAllAdapterBucketStatus() { + List list = bucketService.getAllAdapterBucketStatus(); + return ok(list); + } + + @GetMapping("/adapter/{adapterId}/bucket-status") + public ResponseEntity getAdapterBucketStatus(@PathVariable String adapterId) { + TargetBucketStatusDTO dto = bucketService.getAdapterBucketStatus(adapterId); + return single(dto, "어댑터를 찾을 수 없습니다: " + adapterId); + } + + // ========================================================================= + // 인터페이스 + // ========================================================================= + + @GetMapping("/interface/bucket-status") + public ResponseEntity getAllInterfaceBucketStatus() { + List list = bucketService.getAllInterfaceBucketStatus(); + return ok(list); + } + + @GetMapping("/interface/{interfaceId}/bucket-status") + public ResponseEntity getInterfaceBucketStatus(@PathVariable String interfaceId) { + TargetBucketStatusDTO dto = bucketService.getInterfaceBucketStatus(interfaceId); + return single(dto, "인터페이스를 찾을 수 없습니다: " + interfaceId); + } + + // ========================================================================= + // 클라이언트 + // ========================================================================= + + @GetMapping("/client/bucket-status") + public ResponseEntity getAllClientBucketStatus() { + List list = bucketService.getAllClientBucketStatus(); + return ok(list); + } + + @GetMapping("/client/{clientId}/bucket-status") + public ResponseEntity getClientBucketStatus(@PathVariable String clientId) { + TargetBucketStatusDTO dto = bucketService.getClientBucketStatus(clientId); + return single(dto, "클라이언트를 찾을 수 없습니다: " + clientId); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + private ResponseEntity ok(List list) { + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", list); + result.put("count", list.size()); + return ResponseEntity.ok(result); + } + + private ResponseEntity single(TargetBucketStatusDTO dto, String notFoundMsg) { + if (dto == null) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", notFoundMsg); + return ResponseEntity.ok(error); + } + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", dto); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java new file mode 100644 index 0000000..1ec9a3a --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java @@ -0,0 +1,121 @@ +package com.eactive.eai.manage.inflow; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.InflowTargetVO; +import com.eactive.eai.common.inflow.dual.DualCustomBucket; +import com.eactive.eai.common.inflow.dual.DualInflowControlManager; + +/** + * 어댑터/인터페이스/클라이언트 버킷 상태 조회 서비스. + * + * DualInflowControlManager 가 활성화된 경우에만 동작한다. + * InflowControlManager 가 기본 구현체인 경우 빈 목록 또는 null 을 반환한다. + */ +@Service +public class InflowTargetBucketService { + + // ------------------------------------------------------------------------- + // 어댑터 + // ------------------------------------------------------------------------- + + public TargetBucketStatusDTO getAdapterBucketStatus(String adapterId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + DualCustomBucket bucket = manager.getAdapterBucket(adapterId); + if (bucket == null) return null; + return toDto(adapterId, "ADAPTER", bucket); + } + + public List getAllAdapterBucketStatus() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + return toDtoList("ADAPTER", manager.getAdapterBucketMap()); + } + + // ------------------------------------------------------------------------- + // 인터페이스 + // ------------------------------------------------------------------------- + + public TargetBucketStatusDTO getInterfaceBucketStatus(String interfaceId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + DualCustomBucket bucket = manager.getInterfaceBucket(interfaceId); + if (bucket == null) return null; + return toDto(interfaceId, "INTERFACE", bucket); + } + + public List getAllInterfaceBucketStatus() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + return toDtoList("INTERFACE", manager.getInterfaceBucketMap()); + } + + // ------------------------------------------------------------------------- + // 클라이언트 + // ------------------------------------------------------------------------- + + public TargetBucketStatusDTO getClientBucketStatus(String clientId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + DualCustomBucket bucket = manager.getClientBucket(clientId); + if (bucket == null) return null; + return toDto(clientId, "CLIENT", bucket); + } + + public List getAllClientBucketStatus() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + return toDtoList("CLIENT", manager.getClientBucketMap()); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + private DualInflowControlManager getDualManager() { + InflowControlManager base = InflowControlManager.getInstance(); + return (base instanceof DualInflowControlManager) ? (DualInflowControlManager) base : null; + } + + private TargetBucketStatusDTO toDto(String targetId, String targetType, DualCustomBucket bucket) { + InflowTargetVO vo = bucket.getInflowTargetVo(); + List buckets = new ArrayList<>(); + + if (vo.getThresholdPerSecond() > 0) { + long capacity = vo.getThresholdPerSecond(); + long available = bucket.getPerSecondAvailableTokens(); + if (available < 0) available = capacity; + buckets.add(new GroupBucketStatusDTO.BucketInfo( + "perSecond", capacity, Math.min(available, capacity), "SEC")); + } + + if (vo.getThreshold() > 0) { + long capacity = vo.getThreshold(); + long available = bucket.getThresholdAvailableTokens(); + if (available < 0) available = capacity; + buckets.add(new GroupBucketStatusDTO.BucketInfo( + "threshold", capacity, Math.min(available, capacity), vo.getThresholdTimeUnit())); + } + + TargetBucketStatusDTO dto = new TargetBucketStatusDTO(); + dto.setTargetId(targetId); + dto.setTargetType(targetType); + dto.setActivate(vo.isActivate()); + dto.setBuckets(buckets); + return dto; + } + + private List toDtoList(String targetType, Map bucketMap) { + List result = new ArrayList<>(); + for (Map.Entry entry : bucketMap.entrySet()) { + result.add(toDto(entry.getKey(), targetType, entry.getValue())); + } + return result; + } +} diff --git a/src/main/java/com/eactive/eai/manage/inflow/TargetBucketStatusDTO.java b/src/main/java/com/eactive/eai/manage/inflow/TargetBucketStatusDTO.java new file mode 100644 index 0000000..1f7ed8e --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/TargetBucketStatusDTO.java @@ -0,0 +1,27 @@ +package com.eactive.eai.manage.inflow; + +import java.util.List; + +/** + * 어댑터/인터페이스/클라이언트 버킷 상태 DTO. + * {@link GroupBucketStatusDTO}와 동일한 BucketInfo 구조를 공유한다. + */ +public class TargetBucketStatusDTO { + + private String targetId; // 어댑터명 | 인터페이스ID | 클라이언트ID + private String targetType; // "ADAPTER" | "INTERFACE" | "CLIENT" + private boolean activate; + private List buckets; + + public String getTargetId() { return targetId; } + public void setTargetId(String targetId) { this.targetId = targetId; } + + public String getTargetType() { return targetType; } + public void setTargetType(String targetType) { this.targetType = targetType; } + + public boolean isActivate() { return activate; } + public void setActivate(boolean activate) { this.activate = activate; } + + public List getBuckets() { return buckets; } + public void setBuckets(List buckets) { this.buckets = buckets; } +} From 2eeee58d35938d5ffdccd0df141cf3b1830484e2 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 29 Apr 2026 15:15:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EB=B2=84=ED=82=B7=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20Service/Cont?= =?UTF-8?q?roller=20=EB=8B=A8=EC=9C=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=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()); + } +} From 04aa0df977e84e7b4a51c3390f26e079914e6d25 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:15:55 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20InflowGroupBucketService=20/=20?= =?UTF-8?q?InflowTargetBucketService=20AbstractInflowControlManager=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서브모듈 리팩토링(AbstractInflowControlManager 분리)에 맞춰 InflowControlManager.getInstance() 호출을 InflowControlUtil.getInflowControlManager()로 변경. 반환 타입을 AbstractInflowControlManager로 통일. Co-Authored-By: Claude Sonnet 4.6 --- elink-online-common | 2 +- .../eai/manage/inflow/InflowGroupBucketService.java | 9 +++++---- .../eai/manage/inflow/InflowTargetBucketService.java | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/elink-online-common b/elink-online-common index 9589bc6..437961e 160000 --- a/elink-online-common +++ b/elink-online-common @@ -1 +1 @@ -Subproject commit 9589bc635aac190ddea60f065fe397231fbfcd7b +Subproject commit 437961e7d518df958b2b3109bf19d0c8beec4b3f diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java index ceabb45..d0496ae 100644 --- a/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowGroupBucketService.java @@ -5,15 +5,16 @@ import java.util.List; import org.springframework.stereotype.Service; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; import com.eactive.eai.common.inflow.CustomGroupBucket; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.inflow.InflowGroupVO; @Service public class InflowGroupBucketService { public GroupBucketStatusDTO getGroupBucketStatus(String groupId) { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); InflowGroupVO groupVo = manager.getGroupInflowThreshold(groupId); if (groupVo == null) { @@ -71,11 +72,11 @@ public class InflowGroupBucketService { } public CustomGroupBucket getCustomGroupBucket(String groupId) { - return InflowControlManager.getInstance().getGroupBucket(groupId); + return InflowControlUtil.getInflowControlManager().getGroupBucket(groupId); } public List getAllGroupBucketStatus() { - InflowControlManager manager = InflowControlManager.getInstance(); + AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager(); String[] groupIds = manager.getGroupAllKeys(); List result = new ArrayList<>(); 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 6168aae..5773a9c 100644 --- a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetBucketService.java @@ -6,7 +6,8 @@ import java.util.Map; import org.springframework.stereotype.Service; -import com.eactive.eai.common.inflow.InflowControlManager; +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; import com.eactive.eai.common.inflow.InflowTargetVO; import com.eactive.eai.common.inflow.dual.DualCustomBucket; import com.eactive.eai.common.inflow.dual.DualInflowControlManager; @@ -79,7 +80,7 @@ public class InflowTargetBucketService { // ------------------------------------------------------------------------- protected DualInflowControlManager getDualManager() { - InflowControlManager base = InflowControlManager.getInstance(); + AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); return (base instanceof DualInflowControlManager) ? (DualInflowControlManager) base : null; } From 8bcbee7e524861322e61312b5bff114c1b228899 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 30 Apr 2026 10:16:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EC=96=B4=EB=8C=91=ED=84=B0/?= =?UTF-8?q?=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4/=ED=81=B4?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EC=9C=A0=EB=9F=89?= =?UTF-8?q?=EC=A0=9C=EC=96=B4=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InflowTargetMetricController / InflowTargetMetricService / TargetMetricDTO 추가. 어댑터·인터페이스·클라이언트별 허용/초당거절/임계거절 카운터 조회 및 초기화 API 제공. 단위테스트: InflowTargetMetricServiceTest(26건), InflowTargetMetricControllerTest(16건). Co-Authored-By: Claude Sonnet 4.6 --- .../inflow/InflowTargetMetricController.java | 166 +++++++++ .../inflow/InflowTargetMetricService.java | 157 +++++++++ .../eai/manage/inflow/TargetMetricDTO.java | 44 +++ .../InflowTargetMetricControllerTest.java | 301 ++++++++++++++++ .../inflow/InflowTargetMetricServiceTest.java | 325 ++++++++++++++++++ 5 files changed, 993 insertions(+) create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricController.java create mode 100644 src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricService.java create mode 100644 src/main/java/com/eactive/eai/manage/inflow/TargetMetricDTO.java create mode 100644 src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricControllerTest.java create mode 100644 src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricServiceTest.java diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricController.java b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricController.java new file mode 100644 index 0000000..31bfc90 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricController.java @@ -0,0 +1,166 @@ +package com.eactive.eai.manage.inflow; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 어댑터/인터페이스/클라이언트 유량제어 메트릭 API. + * + * DualInflowControlManager 가 활성화된 환경에서만 정상 응답한다. + * + * GET /manage/inflow/adapter/metric → 전체 어댑터 메트릭 + * GET /manage/inflow/adapter/{adapterId}/metric → 특정 어댑터 메트릭 + * POST /manage/inflow/adapter/metric/reset → 전체 어댑터 메트릭 초기화 + * POST /manage/inflow/adapter/{adapterId}/metric/reset → 특정 어댑터 메트릭 초기화 + * + * GET /manage/inflow/interface/metric → 전체 인터페이스 메트릭 + * GET /manage/inflow/interface/{interfaceId}/metric → 특정 인터페이스 메트릭 + * POST /manage/inflow/interface/metric/reset → 전체 인터페이스 메트릭 초기화 + * POST /manage/inflow/interface/{interfaceId}/metric/reset → 특정 인터페이스 메트릭 초기화 + * + * GET /manage/inflow/client/metric → 전체 클라이언트 메트릭 + * GET /manage/inflow/client/{clientId}/metric → 특정 클라이언트 메트릭 + * POST /manage/inflow/client/metric/reset → 전체 클라이언트 메트릭 초기화 + * POST /manage/inflow/client/{clientId}/metric/reset → 특정 클라이언트 메트릭 초기화 + */ +@RestController +@RequestMapping("/manage/inflow") +public class InflowTargetMetricController { + + @Autowired + private InflowTargetMetricService metricService; + + // ========================================================================= + // 어댑터 + // ========================================================================= + + @GetMapping("/adapter/metric") + public ResponseEntity getAllAdapterMetrics() { + List list = metricService.getAllAdapterMetrics(); + return okList(list); + } + + @GetMapping("/adapter/{adapterId}/metric") + public ResponseEntity getAdapterMetric(@PathVariable String adapterId) { + TargetMetricDTO dto = metricService.getAdapterMetric(adapterId); + return okSingle(dto, "어댑터를 찾을 수 없습니다: " + adapterId); + } + + @PostMapping("/adapter/metric/reset") + public ResponseEntity resetAllAdapterMetrics() { + metricService.resetAllAdapterMetrics(); + return okReset("전체 어댑터 메트릭 초기화 완료"); + } + + @PostMapping("/adapter/{adapterId}/metric/reset") + public ResponseEntity resetAdapterMetric(@PathVariable String adapterId) { + boolean ok = metricService.resetAdapterMetric(adapterId); + return okResetSingle(ok, adapterId, "어댑터"); + } + + // ========================================================================= + // 인터페이스 + // ========================================================================= + + @GetMapping("/interface/metric") + public ResponseEntity getAllInterfaceMetrics() { + List list = metricService.getAllInterfaceMetrics(); + return okList(list); + } + + @GetMapping("/interface/{interfaceId}/metric") + public ResponseEntity getInterfaceMetric(@PathVariable String interfaceId) { + TargetMetricDTO dto = metricService.getInterfaceMetric(interfaceId); + return okSingle(dto, "인터페이스를 찾을 수 없습니다: " + interfaceId); + } + + @PostMapping("/interface/metric/reset") + public ResponseEntity resetAllInterfaceMetrics() { + metricService.resetAllInterfaceMetrics(); + return okReset("전체 인터페이스 메트릭 초기화 완료"); + } + + @PostMapping("/interface/{interfaceId}/metric/reset") + public ResponseEntity resetInterfaceMetric(@PathVariable String interfaceId) { + boolean ok = metricService.resetInterfaceMetric(interfaceId); + return okResetSingle(ok, interfaceId, "인터페이스"); + } + + // ========================================================================= + // 클라이언트 + // ========================================================================= + + @GetMapping("/client/metric") + public ResponseEntity getAllClientMetrics() { + List list = metricService.getAllClientMetrics(); + return okList(list); + } + + @GetMapping("/client/{clientId}/metric") + public ResponseEntity getClientMetric(@PathVariable String clientId) { + TargetMetricDTO dto = metricService.getClientMetric(clientId); + return okSingle(dto, "클라이언트를 찾을 수 없습니다: " + clientId); + } + + @PostMapping("/client/metric/reset") + public ResponseEntity resetAllClientMetrics() { + metricService.resetAllClientMetrics(); + return okReset("전체 클라이언트 메트릭 초기화 완료"); + } + + @PostMapping("/client/{clientId}/metric/reset") + public ResponseEntity resetClientMetric(@PathVariable String clientId) { + boolean ok = metricService.resetClientMetric(clientId); + return okResetSingle(ok, clientId, "클라이언트"); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + private ResponseEntity okList(List list) { + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", list); + result.put("count", list.size()); + return ResponseEntity.ok(result); + } + + private ResponseEntity okSingle(TargetMetricDTO dto, String notFoundMsg) { + if (dto == null) { + Map error = new HashMap<>(); + error.put("success", false); + error.put("message", notFoundMsg); + return ResponseEntity.ok(error); + } + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", dto); + return ResponseEntity.ok(result); + } + + private ResponseEntity okReset(String message) { + Map result = new HashMap<>(); + result.put("success", true); + result.put("message", message); + return ResponseEntity.ok(result); + } + + private ResponseEntity okResetSingle(boolean ok, String targetId, String targetLabel) { + Map result = new HashMap<>(); + result.put("success", ok); + result.put("message", ok + ? "초기화 완료: " + targetId + : targetLabel + "를 찾을 수 없습니다: " + targetId); + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricService.java b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricService.java new file mode 100644 index 0000000..5eadad3 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/InflowTargetMetricService.java @@ -0,0 +1,157 @@ +package com.eactive.eai.manage.inflow; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Service; + +import com.eactive.eai.common.inflow.AbstractInflowControlManager; +import com.eactive.eai.common.inflow.InflowControlUtil; +import com.eactive.eai.common.inflow.InflowTargetVO; +import com.eactive.eai.common.inflow.dual.DualInflowControlManager; + +@Service +public class InflowTargetMetricService { + + // ------------------------------------------------------------------------- + // 어댑터 + // ------------------------------------------------------------------------- + + public TargetMetricDTO getAdapterMetric(String adapterId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + InflowTargetVO vo = manager.getAdapterInflowThreashold(adapterId); + if (vo == null) return null; + return buildDTO(adapterId, "ADAPTER", vo, manager.getAdapterMetrics(adapterId)); + } + + public List getAllAdapterMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + List result = new ArrayList<>(); + for (String id : manager.getAdapterAllKeys()) { + TargetMetricDTO dto = getAdapterMetric(id); + if (dto != null) result.add(dto); + } + return result; + } + + public boolean resetAdapterMetric(String adapterId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return false; + if (manager.getAdapterInflowThreashold(adapterId) == null) return false; + manager.resetAdapterMetrics(adapterId); + return true; + } + + public void resetAllAdapterMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager != null) manager.resetAllAdapterMetrics(); + } + + // ------------------------------------------------------------------------- + // 인터페이스 + // ------------------------------------------------------------------------- + + public TargetMetricDTO getInterfaceMetric(String interfaceId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + InflowTargetVO vo = manager.getInterfaceInflowThreashold(interfaceId); + if (vo == null) return null; + return buildDTO(interfaceId, "INTERFACE", vo, manager.getInterfaceMetrics(interfaceId)); + } + + public List getAllInterfaceMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + List result = new ArrayList<>(); + for (String id : manager.getInterfaceAllKeys()) { + TargetMetricDTO dto = getInterfaceMetric(id); + if (dto != null) result.add(dto); + } + return result; + } + + public boolean resetInterfaceMetric(String interfaceId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return false; + if (manager.getInterfaceInflowThreashold(interfaceId) == null) return false; + manager.resetInterfaceMetrics(interfaceId); + return true; + } + + public void resetAllInterfaceMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager != null) manager.resetAllInterfaceMetrics(); + } + + // ------------------------------------------------------------------------- + // 클라이언트 + // ------------------------------------------------------------------------- + + public TargetMetricDTO getClientMetric(String clientId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return null; + InflowTargetVO vo = manager.getClientInflowThreshold(clientId); + if (vo == null) return null; + return buildDTO(clientId, "CLIENT", vo, manager.getClientMetrics(clientId)); + } + + public List getAllClientMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return new ArrayList<>(); + List result = new ArrayList<>(); + for (String id : manager.getClientBucketMap().keySet()) { + TargetMetricDTO dto = getClientMetric(id); + if (dto != null) result.add(dto); + } + return result; + } + + public boolean resetClientMetric(String clientId) { + DualInflowControlManager manager = getDualManager(); + if (manager == null) return false; + if (manager.getClientInflowThreshold(clientId) == null) return false; + manager.resetClientMetrics(clientId); + return true; + } + + public void resetAllClientMetrics() { + DualInflowControlManager manager = getDualManager(); + if (manager != null) manager.resetAllClientMetrics(); + } + + // ------------------------------------------------------------------------- + // Private helpers + // ------------------------------------------------------------------------- + + protected DualInflowControlManager getDualManager() { + AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager(); + return (base instanceof DualInflowControlManager) ? (DualInflowControlManager) base : null; + } + + private TargetMetricDTO buildDTO(String targetId, String targetType, + InflowTargetVO vo, + DualInflowControlManager.TargetMetrics m) { + TargetMetricDTO dto = new TargetMetricDTO(); + dto.setTargetId(targetId); + dto.setTargetType(targetType); + dto.setActivate(vo.isActivate()); + + if (m != null) { + long allowed = m.allowed.get(); + long rejPs = m.rejectedPerSecond.get(); + long rejTh = m.rejectedThreshold.get(); + long total = allowed + rejPs + rejTh; + dto.setAllowed(allowed); + dto.setRejectedPerSecond(rejPs); + dto.setRejectedThreshold(rejTh); + dto.setTotalRequests(total); + dto.setRejectRatio(total > 0 + ? Math.round((double)(rejPs + rejTh) / total * 10000) / 100.0 + : 0.0); + dto.setLastResetTime(m.lastResetTime); + } + return dto; + } +} diff --git a/src/main/java/com/eactive/eai/manage/inflow/TargetMetricDTO.java b/src/main/java/com/eactive/eai/manage/inflow/TargetMetricDTO.java new file mode 100644 index 0000000..dd74320 --- /dev/null +++ b/src/main/java/com/eactive/eai/manage/inflow/TargetMetricDTO.java @@ -0,0 +1,44 @@ +package com.eactive.eai.manage.inflow; + +/** + * 어댑터/인터페이스/클라이언트 메트릭 DTO. + */ +public class TargetMetricDTO { + + private String targetId; + private String targetType; // "ADAPTER" | "INTERFACE" | "CLIENT" + private boolean activate; + private long allowed; + private long rejectedPerSecond; + private long rejectedThreshold; + private long totalRequests; + private double rejectRatio; + private long lastResetTime; + + public String getTargetId() { return targetId; } + public void setTargetId(String targetId) { this.targetId = targetId; } + + public String getTargetType() { return targetType; } + public void setTargetType(String targetType) { this.targetType = targetType; } + + public boolean isActivate() { return activate; } + public void setActivate(boolean activate) { this.activate = activate; } + + public long getAllowed() { return allowed; } + public void setAllowed(long allowed) { this.allowed = allowed; } + + public long getRejectedPerSecond() { return rejectedPerSecond; } + public void setRejectedPerSecond(long rejectedPerSecond) { this.rejectedPerSecond = rejectedPerSecond; } + + public long getRejectedThreshold() { return rejectedThreshold; } + public void setRejectedThreshold(long rejectedThreshold) { this.rejectedThreshold = rejectedThreshold; } + + public long getTotalRequests() { return totalRequests; } + public void setTotalRequests(long totalRequests) { this.totalRequests = totalRequests; } + + public double getRejectRatio() { return rejectRatio; } + public void setRejectRatio(double rejectRatio) { this.rejectRatio = rejectRatio; } + + public long getLastResetTime() { return lastResetTime; } + public void setLastResetTime(long lastResetTime) { this.lastResetTime = lastResetTime; } +} diff --git a/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricControllerTest.java b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricControllerTest.java new file mode 100644 index 0000000..a7dcd96 --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricControllerTest.java @@ -0,0 +1,301 @@ +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; + +/** + * InflowTargetMetricController 단위 테스트. + * + *

Spring MVC를 기동하지 않고 컨트롤러를 직접 인스턴스화하여 + * 응답 구조(success, data, count, message)를 검증한다. + */ +class InflowTargetMetricControllerTest { + + private InflowTargetMetricController controller; + private InflowTargetMetricService mockService; + + @BeforeEach + void setUp() { + controller = new InflowTargetMetricController(); + mockService = mock(InflowTargetMetricService.class); + ReflectionTestUtils.setField(controller, "metricService", mockService); + } + + // ------------------------------------------------------------------------- + // Helper + // ------------------------------------------------------------------------- + + private TargetMetricDTO makeDTO(String targetId, String targetType, + long allowed, long rejPs, long rejTh) { + TargetMetricDTO dto = new TargetMetricDTO(); + dto.setTargetId(targetId); + dto.setTargetType(targetType); + dto.setActivate(true); + dto.setAllowed(allowed); + dto.setRejectedPerSecond(rejPs); + dto.setRejectedThreshold(rejTh); + long total = allowed + rejPs + rejTh; + dto.setTotalRequests(total); + dto.setRejectRatio(total > 0 ? (double)(rejPs + rejTh) / total * 100 : 0.0); + return dto; + } + + @SuppressWarnings("unchecked") + private Map body(ResponseEntity response) { + return (Map) response.getBody(); + } + + // ========================================================================= + // 어댑터 — 단일 조회 + // ========================================================================= + + @Test + void getAdapterMetric_found_successTrueAndDataPresent() { + TargetMetricDTO dto = makeDTO("A1", "ADAPTER", 900, 60, 40); + when(mockService.getAdapterMetric("A1")).thenReturn(dto); + + ResponseEntity response = controller.getAdapterMetric("A1"); + 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 getAdapterMetric_notFound_successFalseWithMessage() { + when(mockService.getAdapterMetric("GHOST")).thenReturn(null); + + Map body = body(controller.getAdapterMetric("GHOST")); + + assertEquals(Boolean.FALSE, body.get("success")); + assertTrue(body.get("message").toString().contains("GHOST")); + } + + // ========================================================================= + // 어댑터 — 전체 조회 + // ========================================================================= + + @Test + void getAllAdapterMetrics_returnsListAndCount() { + List list = Arrays.asList( + makeDTO("A1", "ADAPTER", 100, 5, 0), + makeDTO("A2", "ADAPTER", 200, 0, 10) + ); + when(mockService.getAllAdapterMetrics()).thenReturn(list); + + Map body = body(controller.getAllAdapterMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(list, body.get("data")); + assertEquals(2, body.get("count")); + } + + @Test + void getAllAdapterMetrics_emptyList_countZero() { + when(mockService.getAllAdapterMetrics()).thenReturn(Collections.emptyList()); + + Map body = body(controller.getAllAdapterMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals(0, body.get("count")); + } + + // ========================================================================= + // 어댑터 — reset + // ========================================================================= + + @Test + void resetAdapterMetric_success_successTrueWithTargetId() { + when(mockService.resetAdapterMetric("A1")).thenReturn(true); + + Map body = body(controller.resetAdapterMetric("A1")); + + assertEquals(Boolean.TRUE, body.get("success")); + assertTrue(body.get("message").toString().contains("A1")); + } + + @Test + void resetAdapterMetric_notFound_successFalseWithTargetId() { + when(mockService.resetAdapterMetric("GHOST")).thenReturn(false); + + Map body = body(controller.resetAdapterMetric("GHOST")); + + assertEquals(Boolean.FALSE, body.get("success")); + assertTrue(body.get("message").toString().contains("GHOST")); + } + + @Test + void resetAllAdapterMetrics_alwaysSuccessTrue() { + doNothing().when(mockService).resetAllAdapterMetrics(); + + Map body = body(controller.resetAllAdapterMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertNotNull(body.get("message")); + verify(mockService).resetAllAdapterMetrics(); + } + + // ========================================================================= + // 인터페이스 — 단일 조회 + // ========================================================================= + + @Test + void getInterfaceMetric_found_successTrueAndDataPresent() { + TargetMetricDTO dto = makeDTO("IF1", "INTERFACE", 500, 20, 10); + when(mockService.getInterfaceMetric("IF1")).thenReturn(dto); + + Map body = body(controller.getInterfaceMetric("IF1")); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(dto, body.get("data")); + } + + @Test + void getInterfaceMetric_notFound_successFalseWithMessage() { + when(mockService.getInterfaceMetric("GHOST")).thenReturn(null); + + Map body = body(controller.getInterfaceMetric("GHOST")); + + assertEquals(Boolean.FALSE, body.get("success")); + assertTrue(body.get("message").toString().contains("GHOST")); + } + + // ========================================================================= + // 인터페이스 — 전체 조회 + // ========================================================================= + + @Test + void getAllInterfaceMetrics_returnsListAndCount() { + List list = Collections.singletonList( + makeDTO("IF1", "INTERFACE", 100, 0, 0)); + when(mockService.getAllInterfaceMetrics()).thenReturn(list); + + Map body = body(controller.getAllInterfaceMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals(1, body.get("count")); + } + + // ========================================================================= + // 인터페이스 — reset + // ========================================================================= + + @Test + void resetInterfaceMetric_success_successTrue() { + when(mockService.resetInterfaceMetric("IF1")).thenReturn(true); + + Map body = body(controller.resetInterfaceMetric("IF1")); + + assertEquals(Boolean.TRUE, body.get("success")); + assertTrue(body.get("message").toString().contains("IF1")); + } + + @Test + void resetAllInterfaceMetrics_callsDelegateOnce() { + doNothing().when(mockService).resetAllInterfaceMetrics(); + + controller.resetAllInterfaceMetrics(); + + verify(mockService, times(1)).resetAllInterfaceMetrics(); + } + + // ========================================================================= + // 클라이언트 — 단일 조회 + // ========================================================================= + + @Test + void getClientMetric_found_successTrueAndDataPresent() { + TargetMetricDTO dto = makeDTO("C1", "CLIENT", 300, 50, 20); + when(mockService.getClientMetric("C1")).thenReturn(dto); + + Map body = body(controller.getClientMetric("C1")); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(dto, body.get("data")); + } + + @Test + void getClientMetric_notFound_successFalseWithMessage() { + when(mockService.getClientMetric("GHOST")).thenReturn(null); + + Map body = body(controller.getClientMetric("GHOST")); + + assertEquals(Boolean.FALSE, body.get("success")); + assertTrue(body.get("message").toString().contains("GHOST")); + } + + // ========================================================================= + // 클라이언트 — 전체 조회 + // ========================================================================= + + @Test + void getAllClientMetrics_returnsListAndCount() { + List list = Arrays.asList( + makeDTO("C1", "CLIENT", 100, 5, 5), + makeDTO("C2", "CLIENT", 200, 0, 0), + makeDTO("C3", "CLIENT", 50, 10, 0) + ); + when(mockService.getAllClientMetrics()).thenReturn(list); + + Map body = body(controller.getAllClientMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertSame(list, body.get("data")); + assertEquals(3, body.get("count")); + } + + @Test + void getAllClientMetrics_emptyList_countZero() { + when(mockService.getAllClientMetrics()).thenReturn(Collections.emptyList()); + + Map body = body(controller.getAllClientMetrics()); + + assertEquals(Boolean.TRUE, body.get("success")); + assertEquals(0, body.get("count")); + } + + // ========================================================================= + // 클라이언트 — reset + // ========================================================================= + + @Test + void resetClientMetric_success_successTrue() { + when(mockService.resetClientMetric("C1")).thenReturn(true); + + Map body = body(controller.resetClientMetric("C1")); + + assertEquals(Boolean.TRUE, body.get("success")); + assertTrue(body.get("message").toString().contains("C1")); + } + + @Test + void resetClientMetric_notFound_successFalse() { + when(mockService.resetClientMetric("GHOST")).thenReturn(false); + + Map body = body(controller.resetClientMetric("GHOST")); + + assertEquals(Boolean.FALSE, body.get("success")); + assertTrue(body.get("message").toString().contains("GHOST")); + } + + @Test + void resetAllClientMetrics_callsDelegateOnce() { + doNothing().when(mockService).resetAllClientMetrics(); + + controller.resetAllClientMetrics(); + + verify(mockService, times(1)).resetAllClientMetrics(); + } +} diff --git a/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricServiceTest.java b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricServiceTest.java new file mode 100644 index 0000000..91c13cb --- /dev/null +++ b/src/test/java/com/eactive/eai/manage/inflow/InflowTargetMetricServiceTest.java @@ -0,0 +1,325 @@ +package com.eactive.eai.manage.inflow; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.eactive.eai.common.inflow.InflowTargetVO; +import com.eactive.eai.common.inflow.dual.DualInflowControlManager; + +/** + * InflowTargetMetricService 단위 테스트. + * + *

getDualManager()를 오버라이드하여 DualInflowControlManager 의존성을 주입한다. + */ +class InflowTargetMetricServiceTest { + + // ------------------------------------------------------------------------- + // 테스트용 서브클래스 + // ------------------------------------------------------------------------- + + private static InflowTargetMetricService serviceWith(DualInflowControlManager manager) { + return new InflowTargetMetricService() { + @Override + protected DualInflowControlManager getDualManager() { return manager; } + }; + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private InflowTargetVO makeVO(String name) { + InflowTargetVO vo = new InflowTargetVO(); + vo.setName(name); + vo.setActivate(true); + return vo; + } + + private DualInflowControlManager.TargetMetrics makeMetrics(long allowed, long rejPs, long rejTh) { + DualInflowControlManager.TargetMetrics m = new DualInflowControlManager.TargetMetrics(); + m.allowed.set(allowed); + m.rejectedPerSecond.set(rejPs); + m.rejectedThreshold.set(rejTh); + return m; + } + + // ========================================================================= + // DualManager null (비활성 환경) + // ========================================================================= + + @Test + void getAdapterMetric_managerIsNull_returnsNull() { + assertNull(serviceWith(null).getAdapterMetric("A1")); + } + + @Test + void getAllAdapterMetrics_managerIsNull_returnsEmptyList() { + assertTrue(serviceWith(null).getAllAdapterMetrics().isEmpty()); + } + + @Test + void resetAdapterMetric_managerIsNull_returnsFalse() { + assertFalse(serviceWith(null).resetAdapterMetric("A1")); + } + + @Test + void resetAllAdapterMetrics_managerIsNull_noException() { + assertDoesNotThrow(() -> serviceWith(null).resetAllAdapterMetrics()); + } + + @Test + void getInterfaceMetric_managerIsNull_returnsNull() { + assertNull(serviceWith(null).getInterfaceMetric("IF1")); + } + + @Test + void getClientMetric_managerIsNull_returnsNull() { + assertNull(serviceWith(null).getClientMetric("C1")); + } + + // ========================================================================= + // 어댑터 — 단일 조회 + // ========================================================================= + + @Test + void getAdapterMetric_notRegistered_returnsNull() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(null); + + assertNull(serviceWith(manager).getAdapterMetric("A1")); + } + + @Test + void getAdapterMetric_found_mapsFields() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + when(manager.getAdapterMetrics("A1")).thenReturn(makeMetrics(900, 60, 40)); + + TargetMetricDTO dto = serviceWith(manager).getAdapterMetric("A1"); + + assertNotNull(dto); + assertEquals("A1", dto.getTargetId()); + assertEquals("ADAPTER", dto.getTargetType()); + assertTrue(dto.isActivate()); + assertEquals(900, dto.getAllowed()); + assertEquals(60, dto.getRejectedPerSecond()); + assertEquals(40, dto.getRejectedThreshold()); + assertEquals(1000, dto.getTotalRequests()); + } + + @Test + void getAdapterMetric_noMetricsYet_returnsZeroCounters() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + when(manager.getAdapterMetrics("A1")).thenReturn(null); + + TargetMetricDTO dto = serviceWith(manager).getAdapterMetric("A1"); + + assertNotNull(dto); + assertEquals(0, dto.getAllowed()); + assertEquals(0, dto.getTotalRequests()); + assertEquals(0.0, dto.getRejectRatio(), 0.001); + } + + @Test + void getAdapterMetric_rejectRatioCalculation() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + // allowed=90, rejPs=5, rejTh=5 → reject=10/100 = 10.00% + when(manager.getAdapterMetrics("A1")).thenReturn(makeMetrics(90, 5, 5)); + + TargetMetricDTO dto = serviceWith(manager).getAdapterMetric("A1"); + + assertEquals(10.0, dto.getRejectRatio(), 0.01); + } + + @Test + void getAdapterMetric_zeroTotal_rejectRatioIsZero() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + when(manager.getAdapterMetrics("A1")).thenReturn(makeMetrics(0, 0, 0)); + + TargetMetricDTO dto = serviceWith(manager).getAdapterMetric("A1"); + + assertEquals(0.0, dto.getRejectRatio(), 0.0001); + } + + // ========================================================================= + // 어댑터 — 전체 조회 + // ========================================================================= + + @Test + void getAllAdapterMetrics_twoEntries_returnsBoth() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterAllKeys()).thenReturn(new String[]{"A1", "A2"}); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + when(manager.getAdapterInflowThreashold("A2")).thenReturn(makeVO("A2")); + when(manager.getAdapterMetrics("A1")).thenReturn(makeMetrics(10, 0, 0)); + when(manager.getAdapterMetrics("A2")).thenReturn(makeMetrics(20, 5, 0)); + + List result = serviceWith(manager).getAllAdapterMetrics(); + + assertEquals(2, result.size()); + assertEquals("A1", result.get(0).getTargetId()); + assertEquals("A2", result.get(1).getTargetId()); + } + + @Test + void getAllAdapterMetrics_noKeys_returnsEmptyList() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterAllKeys()).thenReturn(new String[]{}); + + assertTrue(serviceWith(manager).getAllAdapterMetrics().isEmpty()); + } + + @Test + void getAllAdapterMetrics_notRegisteredSkipped() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterAllKeys()).thenReturn(new String[]{"A1", "GHOST"}); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + when(manager.getAdapterInflowThreashold("GHOST")).thenReturn(null); + when(manager.getAdapterMetrics("A1")).thenReturn(null); + + List result = serviceWith(manager).getAllAdapterMetrics(); + + assertEquals(1, result.size()); + assertEquals("A1", result.get(0).getTargetId()); + } + + // ========================================================================= + // 어댑터 — reset + // ========================================================================= + + @Test + void resetAdapterMetric_notRegistered_returnsFalse() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(null); + + assertFalse(serviceWith(manager).resetAdapterMetric("A1")); + verify(manager, never()).resetAdapterMetrics(any()); + } + + @Test + void resetAdapterMetric_success_returnsTrueAndCallsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getAdapterInflowThreashold("A1")).thenReturn(makeVO("A1")); + + assertTrue(serviceWith(manager).resetAdapterMetric("A1")); + verify(manager).resetAdapterMetrics("A1"); + } + + @Test + void resetAllAdapterMetrics_callsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + + serviceWith(manager).resetAllAdapterMetrics(); + + verify(manager).resetAllAdapterMetrics(); + } + + // ========================================================================= + // 인터페이스 — 단일 조회 + // ========================================================================= + + @Test + void getInterfaceMetric_found_mapsFields() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getInterfaceInflowThreashold("IF1")).thenReturn(makeVO("IF1")); + when(manager.getInterfaceMetrics("IF1")).thenReturn(makeMetrics(50, 10, 5)); + + TargetMetricDTO dto = serviceWith(manager).getInterfaceMetric("IF1"); + + assertNotNull(dto); + assertEquals("IF1", dto.getTargetId()); + assertEquals("INTERFACE", dto.getTargetType()); + assertEquals(50, dto.getAllowed()); + assertEquals(65, dto.getTotalRequests()); + } + + @Test + void getInterfaceMetric_notRegistered_returnsNull() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getInterfaceInflowThreashold("IF1")).thenReturn(null); + + assertNull(serviceWith(manager).getInterfaceMetric("IF1")); + } + + @Test + void resetInterfaceMetric_success_returnsTrueAndCallsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getInterfaceInflowThreashold("IF1")).thenReturn(makeVO("IF1")); + + assertTrue(serviceWith(manager).resetInterfaceMetric("IF1")); + verify(manager).resetInterfaceMetrics("IF1"); + } + + @Test + void resetInterfaceMetric_notRegistered_returnsFalse() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getInterfaceInflowThreashold("IF1")).thenReturn(null); + + assertFalse(serviceWith(manager).resetInterfaceMetric("IF1")); + } + + @Test + void resetAllInterfaceMetrics_callsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + serviceWith(manager).resetAllInterfaceMetrics(); + verify(manager).resetAllInterfaceMetrics(); + } + + // ========================================================================= + // 클라이언트 — 단일 조회 + // ========================================================================= + + @Test + void getClientMetric_found_mapsFields() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getClientInflowThreshold("C1")).thenReturn(makeVO("C1")); + when(manager.getClientMetrics("C1")).thenReturn(makeMetrics(200, 30, 20)); + + TargetMetricDTO dto = serviceWith(manager).getClientMetric("C1"); + + assertNotNull(dto); + assertEquals("C1", dto.getTargetId()); + assertEquals("CLIENT", dto.getTargetType()); + assertEquals(200, dto.getAllowed()); + assertEquals(250, dto.getTotalRequests()); + } + + @Test + void getClientMetric_notRegistered_returnsNull() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getClientInflowThreshold("C1")).thenReturn(null); + + assertNull(serviceWith(manager).getClientMetric("C1")); + } + + @Test + void resetClientMetric_success_returnsTrueAndCallsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getClientInflowThreshold("C1")).thenReturn(makeVO("C1")); + + assertTrue(serviceWith(manager).resetClientMetric("C1")); + verify(manager).resetClientMetrics("C1"); + } + + @Test + void resetClientMetric_notRegistered_returnsFalse() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + when(manager.getClientInflowThreshold("C1")).thenReturn(null); + + assertFalse(serviceWith(manager).resetClientMetric("C1")); + } + + @Test + void resetAllClientMetrics_callsManager() { + DualInflowControlManager manager = mock(DualInflowControlManager.class); + serviceWith(manager).resetAllClientMetrics(); + verify(manager).resetAllClientMetrics(); + } +}