From 74780e8610f3c1cb9a35f6eb946873185837c505 Mon Sep 17 00:00:00 2001 From: Marc Giffing Date: Mon, 11 Mar 2024 19:45:01 +0100 Subject: [PATCH] Support for Method level @RateLimiting annoation #250 --- .../caffeine/CaffeineGeneralSuiteTest.java | 4 +- .../ehcache/EhcacheGeneralSuiteTest.java | 4 +- examples/general-tests/pom.xml | 5 ++ .../filter/method/MethodRateLimitTest.java | 88 +++++++++++++++++++ .../filter/method/MethodTestApplication.java | 17 ++++ .../tests/filter/method/MethodTestSuite.java | 11 +++ .../tests/filter/method/TestService.java | 45 ++++++++++ 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodRateLimitTest.java create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestApplication.java create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestSuite.java create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/TestService.java diff --git a/examples/caffeine/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/caffeine/CaffeineGeneralSuiteTest.java b/examples/caffeine/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/caffeine/CaffeineGeneralSuiteTest.java index c9b2d3b9..9dce3395 100644 --- a/examples/caffeine/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/caffeine/CaffeineGeneralSuiteTest.java +++ b/examples/caffeine/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/caffeine/CaffeineGeneralSuiteTest.java @@ -1,12 +1,14 @@ package com.giffing.bucket4j.spring.boot.starter.examples.caffeine; +import com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method.MethodTestSuite; import com.giffing.bucket4j.spring.boot.starter.general.tests.filter.servlet.ServletTestSuite; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @Suite @SelectClasses({ - ServletTestSuite.class + ServletTestSuite.class, + MethodTestSuite.class }) public class CaffeineGeneralSuiteTest { } diff --git a/examples/ehcache/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/EhcacheGeneralSuiteTest.java b/examples/ehcache/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/EhcacheGeneralSuiteTest.java index a07b9d2c..7821b066 100644 --- a/examples/ehcache/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/EhcacheGeneralSuiteTest.java +++ b/examples/ehcache/src/test/java/com/giffing/bucket4j/spring/boot/starter/examples/ehcache/EhcacheGeneralSuiteTest.java @@ -1,12 +1,14 @@ package com.giffing.bucket4j.spring.boot.starter.examples.ehcache; +import com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method.MethodTestSuite; import com.giffing.bucket4j.spring.boot.starter.general.tests.filter.servlet.ServletTestSuite; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @Suite @SelectClasses({ - ServletTestSuite.class + ServletTestSuite.class, + MethodTestSuite.class, }) public class EhcacheGeneralSuiteTest { } diff --git a/examples/general-tests/pom.xml b/examples/general-tests/pom.xml index 53d38e6d..5f021d70 100644 --- a/examples/general-tests/pom.xml +++ b/examples/general-tests/pom.xml @@ -23,6 +23,11 @@ org.springframework.boot spring-boot-starter-test + + org.springframework.boot + spring-boot-starter-aop + provided + org.springframework.boot spring-boot-starter-validation diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodRateLimitTest.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodRateLimitTest.java new file mode 100644 index 00000000..f1a4b80e --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodRateLimitTest.java @@ -0,0 +1,88 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method; + +import com.giffing.bucket4j.spring.boot.starter.context.RateLimitException; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(properties = { + "debug=true", + "bucket4j.methods[0].name=default", + "bucket4j.methods[0].cache-name=buckets", + "bucket4j.methods[0].rate-limit.bandwidths[0].capacity=5", + "bucket4j.methods[0].rate-limit.bandwidths[0].time=10", + "bucket4j.methods[0].rate-limit.bandwidths[0].unit=seconds", + "bucket4j.methods[0].rate-limit.bandwidths[0].refill-speed=greedy", +}) +@RequiredArgsConstructor +@DirtiesContext +public class MethodRateLimitTest { + + @Autowired + private TestService testService; + + + @Test + public void assert_rate_limit_with_execute_condition_matches() { + for(int i = 0; i < 5; i++) { + // rate limit executed because it's not the admin + testService.withExecuteCondition("normal_user"); + } + assertThrows(RateLimitException.class, () -> testService.withExecuteCondition("normal_user")); + } + + @Test + public void assert_no_rate_limit_with_execute_condition_does_not_match() { + assertAll(() -> { + for(int i = 0; i < 10; i++) { + // rate limit not executed for admin parameter + testService.withExecuteCondition("admin"); + } + }); + } + + @Test + public void assert_rate_limit_with_fallback_method() { + for(int i = 0; i < 5; i++) { + assertEquals("normal-method-executed;param:my-test", testService.withFallbackMethod("my-test")); + } + // no exception is thrown. fall back method is executed + assertEquals("fallback-method-executed;param:my-test", testService.withFallbackMethod("my-test")); + } + + @Test + public void assert_rate_limit_with_skip_condition_does_not_match() { + for(int i = 0; i < 5; i++) { + // skip condition does not match. rate limit is performed + testService.withSkipCondition("normal_user"); + } + assertThrows(RateLimitException.class, () -> testService.withSkipCondition("normal_user")); + } + + @Test + public void assert_no_rate_limit_with_skip_condition_matches() { + assertAll(() -> { + for(int i = 0; i < 10; i++) { + // no token consumption. admin is skipped + testService.withSkipCondition("admin"); + } + }); + } + + @Test + public void assert_rate_limit_with_cache_key() { + for(int i = 0; i < 5; i++) { + // rate limit by parameter value + testService.withCacheKey("key1"); + testService.withCacheKey("key2"); + // all tokens consumed + } + assertThrows(RateLimitException.class, () -> testService.withCacheKey("key1")); + assertThrows(RateLimitException.class, () -> testService.withCacheKey("key2")); + } + +} diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestApplication.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestApplication.java new file mode 100644 index 00000000..39ba101e --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestApplication.java @@ -0,0 +1,17 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@SpringBootApplication +@EnableCaching +@EnableAspectJAutoProxy +public class MethodTestApplication { + + public static void main(String[] args) { + SpringApplication.run(MethodTestApplication.class, args); + } + +} \ No newline at end of file diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestSuite.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestSuite.java new file mode 100644 index 00000000..a0e9764e --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/MethodTestSuite.java @@ -0,0 +1,11 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({ + MethodRateLimitTest.class +}) +public class MethodTestSuite { +} diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/TestService.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/TestService.java new file mode 100644 index 00000000..5ec5c18e --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/filter/method/TestService.java @@ -0,0 +1,45 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.filter.method; + +import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class TestService { + + + @RateLimiting( + name = "default", + executeCondition = "#myParamName != 'admin'") + public String withExecuteCondition(String myParamName) { + log.info("Method withExecuteCondition with Param {} executed", myParamName); + return myParamName; + } + + @RateLimiting( + name = "default", + skipCondition = "#myParamName eq 'admin'") + public String withSkipCondition(String myParamName) { + log.info("Method withSkipCondition with Param {} executed", myParamName); + return myParamName; + } + + @RateLimiting( + name = "default", + cacheKey = "#cacheKey") + public String withCacheKey(String cacheKey) { + log.info("Method withCacheKey with Param {} executed", cacheKey); + return cacheKey; + } + + @RateLimiting(name = "default", cacheKey = "'normal'", fallbackMethodName = "fallbackMethod") + public String withFallbackMethod(String myParamName) { + return "normal-method-executed;param:" + myParamName; + } + + public String fallbackMethod(String myParamName) { + return "fallback-method-executed;param:" + myParamName; + } + +}