map = new ConcurrentHashMap<>();
+ map.put("C1", passDualBucket("C1"));
+ injectClientMap(map);
+
+ manager.isClientPassDetail("C1");
+ assertNotNull(manager.getClientMetrics("C1"));
+
+ manager.removeClient("C1");
+
+ assertNull(manager.getClientMetrics("C1"));
+ }
+}
diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java
new file mode 100644
index 0000000..c1a77c3
--- /dev/null
+++ b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMakeBucketTest.java
@@ -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() 버킷 생성 동작 검증.
+ *
+ * 검증 포인트:
+ * - 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)));
+ }
+}
diff --git a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
index c52f309..be4280c 100644
--- a/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
+++ b/src/test/java/com/eactive/eai/common/inflow/dual/DualInflowControlManagerMetricsTest.java
@@ -21,9 +21,8 @@ import io.github.bucket4j.local.LocalBucket;
/**
* DualInflowControlManager 메트릭 단위 테스트.
*
- *
start() 없이 버킷 맵을 ReflectionTestUtils로 직접 주입하여
- * isAdapterPassDetail / isInterfacePassDetail / isClientPassDetail / isGroupPass() 의
- * 카운팅 동작과 getXxxMetrics / resetXxxMetrics 메서드를 검증한다.
+ *
어댑터/인터페이스/그룹 메트릭을 검증한다.
+ * 클라이언트 메트릭은 ClientDualInflowControlManagerMetricsTest 에서 검증한다.
*/
class DualInflowControlManagerMetricsTest {
@@ -100,10 +99,6 @@ class DualInflowControlManagerMetricsTest {
ReflectionTestUtils.setField(manager, "interfaceBucketMap", map);
}
- private void injectClientMap(Map map) {
- ReflectionTestUtils.setField(manager, "clientBucketMap", map);
- }
-
private void injectGroupBucketList(Map 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 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 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 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 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 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"));
- }
}
diff --git a/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java b/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java
deleted file mode 100644
index ac6853d..0000000
--- a/src/test/java/com/eactive/eai/inbound/processor/DualRequestProcessorTest.java
+++ /dev/null
@@ -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 단위 테스트.
- *
- * RequestProcessor 클래스는 static 초기화 블록에서 EAIServerManager.getInstance()를 호출한다.
- * @BeforeAll에서 mock ApplicationContext를 주입한 뒤 DualRequestProcessor를 최초 로딩시킨다.
- *
- *
주의: 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);
- }
-}
diff --git a/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql b/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql
index d55aeb7..3b97c89 100644
--- a/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql
+++ b/src/test/resources/com/eactive/eai/common/inflow/init_inflow_control_group.sql
@@ -1,2 +1,3 @@
INSERT INTO inflow_control_group (group_id, group_name, threshold, threshold_per_second, threshold_time_unit, use_yn) VALUES
- ('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1');
+ ('test-group-001', '테스트 그룹', 100, 10, 'MIN', '1'),
+ ('test-group-quota', '쿼터전용 그룹', 5, 0, 'MIN', '1');
diff --git a/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql b/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql
index ac25a7f..ec220ab 100644
--- a/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql
+++ b/src/test/resources/com/eactive/eai/common/inflow/init_tseaifr09.sql
@@ -1,6 +1,8 @@
-INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES
- ('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
- ('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
- ('01','_TST_IN_NET_AsS',1,'1',0,NULL),
- ('02','FASSFEP00000004S2',10,'1',0,NULL),
- ('02','FDASFEP00000009S2',2,'1',0,NULL);
\ No newline at end of file
+INSERT INTO tseaifr09 (type,name,threshold,useyn,thresholdpersecond,thresholdtimeunit) VALUES
+ ('01','_AA0_IN_NET_AsS',10,'1',0,NULL),
+ ('01','_AB0_IN_NET_AsS',3,'1',0,NULL),
+ ('01','_TST_IN_NET_AsS',1,'1',0,NULL),
+ ('02','FASSFEP00000004S2',10,'1',0,NULL),
+ ('02','FDASFEP00000009S2',2,'1',0,NULL),
+ ('01','TEST_ADAPTER',5,'1',0,'MIN'),
+ ('02','TEST_INTERFACE',5,'1',0,'MIN');
\ No newline at end of file