Merge branch 'feature/inflow-control-improvement' into master

This commit is contained in:
curry772
2026-05-08 10:51:21 +09:00
17 changed files with 504 additions and 481 deletions
@@ -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);
@@ -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"));
}
}
@@ -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"));
}
}
@@ -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)));
}
}
@@ -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);
}
}