Merge branch 'feature/inflow-control-improvement' into master
This commit is contained in:
+5
-5
@@ -11,7 +11,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
|
||||
/**
|
||||
* ReloadInflowClientControlCommand 단위 테스트.
|
||||
@@ -21,11 +21,11 @@ import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
*/
|
||||
class ReloadInflowClientControlCommandTest {
|
||||
|
||||
private DualInflowControlManager mockDualManager;
|
||||
private ClientDualInflowControlManager mockDualManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockDualManager = mock(DualInflowControlManager.class);
|
||||
mockDualManager = mock(ClientDualInflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
@@ -51,11 +51,11 @@ class ReloadInflowClientControlCommandTest {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DualInflowControlManager 비활성
|
||||
// ClientDualInflowControlManager 비활성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_notDualManager_throwsCommandException() {
|
||||
void execute_notClientDualManager_throwsCommandException() {
|
||||
InflowControlManager baseMgr = mock(InflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
|
||||
|
||||
|
||||
+5
-5
@@ -11,7 +11,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
|
||||
/**
|
||||
* RemoveInflowClientControlCommand 단위 테스트.
|
||||
@@ -21,11 +21,11 @@ import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
*/
|
||||
class RemoveInflowClientControlCommandTest {
|
||||
|
||||
private DualInflowControlManager mockDualManager;
|
||||
private ClientDualInflowControlManager mockDualManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockDualManager = mock(DualInflowControlManager.class);
|
||||
mockDualManager = mock(ClientDualInflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", null);
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@ class RemoveInflowClientControlCommandTest {
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DualInflowControlManager 비활성
|
||||
// ClientDualInflowControlManager 비활성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void execute_notDualManager_throwsCommandException() {
|
||||
void execute_notClientDualManager_throwsCommandException() {
|
||||
InflowControlManager baseMgr = mock(InflowControlManager.class);
|
||||
ReflectionTestUtils.setField(InflowControlUtil.class, "cachedInflowControlManager", baseMgr);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.eactive.eai.common.inflow;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -36,9 +38,13 @@ class InflowControlManagerTest {
|
||||
InflowControlManager inflowControlManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws LifecycleException {
|
||||
void setUp() throws Exception {
|
||||
if (!inflowControlManager.isStarted()) {
|
||||
inflowControlManager.start();
|
||||
} else {
|
||||
inflowControlManager.reloadAdapter();
|
||||
inflowControlManager.reloadInterface();
|
||||
inflowControlManager.reloadGroup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +63,22 @@ class InflowControlManagerTest {
|
||||
assertNull(inflowControlManager.getGroupBucket("non-existent-group"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void adapter_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isAdapterPass("TEST_ADAPTER"));
|
||||
assertFalse(inflowControlManager.isAdapterPass("TEST_ADAPTER"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void interface_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertTrue(inflowControlManager.isInterfacePass("TEST_INTERFACE"));
|
||||
assertFalse(inflowControlManager.isInterfacePass("TEST_INTERFACE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void group_spikePasses_thenBurstBlocked() {
|
||||
for (int i = 0; i < 5; i++) assertNull(inflowControlManager.isGroupPass("test-group-quota"));
|
||||
assertEquals(CustomGroupBucket.RESULT_BLOCKED_THRESHOLD, inflowControlManager.isGroupPass("test-group-quota"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* ClientDualInflowControlManager 클라이언트 메트릭 단위 테스트.
|
||||
*/
|
||||
class ClientDualInflowControlManagerMetricsTest {
|
||||
|
||||
private ClientDualInflowControlManager manager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
manager = new ClientDualInflowControlManager();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private InflowTargetVO makeTargetVO(String name) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(100);
|
||||
vo.setThreshold(10000);
|
||||
vo.setThresholdTimeUnit("DAY");
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private LocalBucket stableBucket(long capacity) {
|
||||
return Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(capacity, Duration.ofHours(1)))
|
||||
.build();
|
||||
}
|
||||
|
||||
private LocalBucket exhaustedBucket(long capacity) {
|
||||
LocalBucket b = stableBucket(capacity);
|
||||
b.tryConsume(capacity);
|
||||
return b;
|
||||
}
|
||||
|
||||
private DualCustomBucket passDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedPerSecondDualBucket(String name) {
|
||||
return new DualCustomBucket(exhaustedBucket(1), null, makeTargetVO(name));
|
||||
}
|
||||
|
||||
private DualCustomBucket blockedThresholdDualBucket(String name) {
|
||||
return new DualCustomBucket(stableBucket(100), exhaustedBucket(1), makeTargetVO(name));
|
||||
}
|
||||
|
||||
private void injectClientMap(Map<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// isClientPassDetail() — 카운터 증가
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
manager.isClientPassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getClientMetrics("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_pass_incrementsAllowed() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", blockedThresholdDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_blockedPerSecond_incrementsRejectedPerSecond() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", blockedPerSecondDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").rejectedPerSecond.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAllClientMetrics — 조회 및 불변성
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void getAllClientMetrics_empty_returnsEmptyMap() {
|
||||
assertTrue(manager.getAllClientMetrics().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllClientMetrics_afterCalls_containsEntries() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
map.put("C2", passDualBucket("C2"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.isClientPassDetail("C2");
|
||||
|
||||
Map<String, DualInflowControlManager.TargetMetrics> all = manager.getAllClientMetrics();
|
||||
assertEquals(2, all.size());
|
||||
assertTrue(all.containsKey("C1"));
|
||||
assertTrue(all.containsKey("C2"));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// resetClientMetrics / resetAllClientMetrics
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void resetClientMetrics_resetsTargetClient() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.resetClientMetrics("C1");
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllClientMetrics_resetsAll() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
map.put("C2", passDualBucket("C2"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.isClientPassDetail("C2");
|
||||
manager.resetAllClientMetrics();
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// removeClient — 메트릭 자동 삭제
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void removeClient_cleansUpMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
assertNotNull(manager.getClientMetrics("C1"));
|
||||
|
||||
manager.removeClient("C1");
|
||||
|
||||
assertNull(manager.getClientMetrics("C1"));
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* DualInflowControlManager.makeBucket() 버킷 생성 동작 검증.
|
||||
*
|
||||
* <p>검증 포인트:
|
||||
* - threshold/perSecond 버킷이 실제로 생성되는지
|
||||
* - 할당량 이내 spike는 통과하고 초과 burst는 차단되는지
|
||||
* - perSecond/threshold 중 어느 쪽이 binding 제약인지 구분되는지
|
||||
*/
|
||||
class DualInflowControlManagerMakeBucketTest {
|
||||
|
||||
private final DualInflowControlManager manager = new DualInflowControlManager();
|
||||
|
||||
private InflowTargetVO vo(long perSecond, long threshold, String timeUnit) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName("TEST");
|
||||
vo.setThresholdPerSecond(perSecond);
|
||||
vo.setThreshold(threshold);
|
||||
vo.setThresholdTimeUnit(timeUnit);
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// threshold 단독 (5건/분)
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void thresholdOnly_spikePasses_thenBurstBlocked() {
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(0, 5, "MIN"));
|
||||
|
||||
for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// perSecond 단독
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void perSecondOnly_spikePasses_thenBurstBlocked() {
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(5, 0, null));
|
||||
|
||||
for (int i = 0; i < 5; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// perSecond + threshold 동시 설정 — binding 제약 구분
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void both_perSecondIsBinding_blockedByPerSecond() {
|
||||
// perSecond=3 이 binding: threshold=100/MIN은 여유 있음
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(3, 100, "MIN"));
|
||||
|
||||
for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND, bucket.tryConsume());
|
||||
}
|
||||
|
||||
@Test
|
||||
void both_thresholdIsBinding_blockedByThreshold() {
|
||||
// threshold=3/MIN 이 binding: perSecond=100은 여유 있음
|
||||
DualCustomBucket bucket = manager.makeBucket(vo(100, 3, "MIN"));
|
||||
|
||||
for (int i = 0; i < 3; i++) assertNull(bucket.tryConsume());
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD, bucket.tryConsume());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 비활성 / 조건 미충족 → null 반환
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void inactiveVO_returnsNull() {
|
||||
InflowTargetVO vo = vo(5, 0, null);
|
||||
vo.setActivate(false);
|
||||
assertNull(manager.makeBucket(vo));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noLimitsSet_returnsNull() {
|
||||
assertNull(manager.makeBucket(vo(0, 0, null)));
|
||||
}
|
||||
}
|
||||
+3
-88
@@ -21,9 +21,8 @@ import io.github.bucket4j.local.LocalBucket;
|
||||
/**
|
||||
* DualInflowControlManager 메트릭 단위 테스트.
|
||||
*
|
||||
* <p>start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여
|
||||
* isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의
|
||||
* 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다.
|
||||
* <p>어댑터/인터페이스/그룹 메트릭을 검증한다.
|
||||
* 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
|
||||
*/
|
||||
class DualInflowControlManagerMetricsTest {
|
||||
|
||||
@@ -100,10 +99,6 @@ class DualInflowControlManagerMetricsTest {
|
||||
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
|
||||
}
|
||||
|
||||
private void injectClientMap(Map<String, DualCustomBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
|
||||
}
|
||||
|
||||
private void injectGroupBucketList(Map<String, CustomGroupBucket> map) {
|
||||
ReflectionTestUtils.setField(manager, "groupBucketList", map);
|
||||
}
|
||||
@@ -358,7 +353,6 @@ class DualInflowControlManagerMetricsTest {
|
||||
|
||||
@Test
|
||||
void isAdapterPassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
// 버킷 없는 어댑터는 메트릭 맵에 항목을 생성하지 않음
|
||||
manager.isAdapterPassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getAdapterMetrics("UNKNOWN"));
|
||||
@@ -473,39 +467,6 @@ class DualInflowControlManagerMetricsTest {
|
||||
assertEquals(1, manager.getInterfaceMetrics("IF2").rejectedPerSecond.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// isClientPassDetail() — 카운터 증가
|
||||
// =========================================================================
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_bucketNotRegistered_noMetricsCreated() {
|
||||
manager.isClientPassDetail("UNKNOWN");
|
||||
|
||||
assertNull(manager.getClientMetrics("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_pass_incrementsAllowed() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void isClientPassDetail_blockedThreshold_incrementsRejectedThreshold() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", blockedThresholdDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
|
||||
assertEquals(1, manager.getClientMetrics("C1").rejectedThreshold.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// getAllXxxMetrics — 조회 및 불변성
|
||||
// =========================================================================
|
||||
@@ -537,11 +498,6 @@ class DualInflowControlManagerMetricsTest {
|
||||
assertTrue(manager.getAllInterfaceMetrics().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAllClientMetrics_empty_returnsEmptyMap() {
|
||||
assertTrue(manager.getAllClientMetrics().isEmpty());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// resetXxxMetrics / resetAllXxxMetrics
|
||||
// =========================================================================
|
||||
@@ -560,7 +516,7 @@ class DualInflowControlManagerMetricsTest {
|
||||
manager.resetAdapterMetrics("A1");
|
||||
|
||||
assertEquals(0, manager.getAdapterMetrics("A1").allowed.get());
|
||||
assertEquals(1, manager.getAdapterMetrics("A2").allowed.get()); // 영향 없음
|
||||
assertEquals(1, manager.getAdapterMetrics("A2").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -601,33 +557,6 @@ class DualInflowControlManagerMetricsTest {
|
||||
assertDoesNotThrow(() -> manager.resetAllInterfaceMetrics());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetClientMetrics_resetsTargetClient() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.resetClientMetrics("C1");
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetAllClientMetrics_resetsAll() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
map.put("C2", passDualBucket("C2"));
|
||||
injectClientMap(map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
manager.isClientPassDetail("C2");
|
||||
manager.resetAllClientMetrics();
|
||||
|
||||
assertEquals(0, manager.getClientMetrics("C1").allowed.get());
|
||||
assertEquals(0, manager.getClientMetrics("C2").allowed.get());
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// remove 시 메트릭 자동 삭제
|
||||
// =========================================================================
|
||||
@@ -659,18 +588,4 @@ class DualInflowControlManagerMetricsTest {
|
||||
|
||||
assertNull(manager.getInterfaceMetrics("IF1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeClient_cleansUpMetrics() {
|
||||
Map<String, DualCustomBucket> map = new ConcurrentHashMap<>();
|
||||
map.put("C1", passDualBucket("C1"));
|
||||
ReflectionTestUtils.setField(manager, "clientBucketMap", map);
|
||||
|
||||
manager.isClientPassDetail("C1");
|
||||
assertNotNull(manager.getClientMetrics("C1"));
|
||||
|
||||
manager.removeClient("C1");
|
||||
|
||||
assertNull(manager.getClientMetrics("C1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
package com.eactive.eai.inbound.processor;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.dual.DualBucket;
|
||||
import com.eactive.eai.common.inflow.dual.DualCustomBucket;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
|
||||
/**
|
||||
* DualRequestProcessor 단위 테스트.
|
||||
*
|
||||
* <p>RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다.
|
||||
* @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DualRequestProcessor를 최초 로딩시킨다.
|
||||
*
|
||||
* <p>주의: DualRequestProcessor를 필드 타입으로 선언하면 테스트 클래스 로딩 시 함께 로딩되어
|
||||
* @BeforeAll 실행 전에 NPE가 발생한다. 반드시 메서드 본문 내에서만 참조해야 한다.
|
||||
*/
|
||||
class DualRequestProcessorTest {
|
||||
|
||||
@BeforeAll
|
||||
static void setupMockApplicationContext() {
|
||||
ApplicationContext mockContext = mock(ApplicationContext.class);
|
||||
EAIServerManager mockServerManager = mock(EAIServerManager.class);
|
||||
when(mockContext.getBean(EAIServerManager.class)).thenReturn(mockServerManager);
|
||||
when(mockServerManager.getLocalServerName()).thenReturn("TEST-SERVER");
|
||||
when(mockServerManager.getGroupInstId()).thenReturn("TEST-INST");
|
||||
|
||||
ReflectionTestUtils.setField(ApplicationContextProvider.class, "context", mockContext);
|
||||
}
|
||||
|
||||
private InflowTargetVO makeVO(String name, long perSec, long threshold, String unit) {
|
||||
InflowTargetVO vo = new InflowTargetVO();
|
||||
vo.setName(name);
|
||||
vo.setThresholdPerSecond(perSec);
|
||||
vo.setThreshold(threshold);
|
||||
vo.setThresholdTimeUnit(unit);
|
||||
vo.setActivate(true);
|
||||
return vo;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// checkAdapterInflow
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(null);
|
||||
|
||||
assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
verify(mockBucket).isAdapterPassDetail("ADAPTER_A");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_blockedPerSecond() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
|
||||
processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_dualBucket_blockedThreshold() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isAdapterPassDetail("ADAPTER_A")).thenReturn(DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_THRESHOLD,
|
||||
processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_nonDualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(true);
|
||||
|
||||
assertNull(processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkAdapterInflow_nonDualBucket_blocked_returnsBlocked() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isAdapterPass("ADAPTER_A")).thenReturn(false);
|
||||
|
||||
assertEquals("BLOCKED", processor.checkAdapterInflow(mockBucket, "ADAPTER_A"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// checkInterfaceInflow
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_dualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(null);
|
||||
|
||||
assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
verify(mockBucket).isInterfacePassDetail("IF_001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_dualBucket_blockedPerSecond() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
DualBucket mockBucket = mock(DualBucket.class);
|
||||
when(mockBucket.isInterfacePassDetail("IF_001")).thenReturn(DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals(DualCustomBucket.RESULT_BLOCKED_PER_SECOND,
|
||||
processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_nonDualBucket_pass_returnsNull() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isInterfacePass("IF_001")).thenReturn(true);
|
||||
|
||||
assertNull(processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void checkInterfaceInflow_nonDualBucket_blocked_returnsBlocked() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
Bucket mockBucket = mock(Bucket.class);
|
||||
when(mockBucket.isInterfacePass("IF_001")).thenReturn(false);
|
||||
|
||||
assertEquals("BLOCKED", processor.checkInterfaceInflow(mockBucket, "IF_001"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// buildInflowTargetErrorMsg
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_perSecond_includesReqPerSec() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_PER_SECOND);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API: 30req/sec]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_threshold_includesReqPerUnit() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API: 1000req/MIN]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_blockedFallback_simpleFormat() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, "BLOCKED");
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_nullBlockedType_simpleFormat() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("결제API", 30, 1000, "MIN");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, null);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [결제API]", msg);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInflowTargetErrorMsg_hourUnit() {
|
||||
DualRequestProcessor processor = new DualRequestProcessor();
|
||||
InflowTargetVO vo = makeVO("조회API", 10, 500, "HOUR");
|
||||
|
||||
String msg = processor.buildInflowTargetErrorMsg(vo, DualCustomBucket.RESULT_BLOCKED_THRESHOLD);
|
||||
|
||||
assertEquals("API 호출 한도 초과 [조회API: 500req/HOUR]", msg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user