diff --git a/spring-cloud/spring-cloud-zuul-ratelimit/pom.xml b/spring-cloud/spring-cloud-zuul-ratelimit/pom.xml new file mode 100644 index 0000000000..46139b4f57 --- /dev/null +++ b/spring-cloud/spring-cloud-zuul-ratelimit/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + com.baeldung.spring.cloud + spring-cloud-zuul-ratelimit + 0.0.1-SNAPSHOT + jar + + spring-cloud-zuul-ratelimit + Demo project for Spring Boot + + + org.springframework.boot + spring-boot-starter-parent + 2.0.6.RELEASE + + + + + UTF-8 + UTF-8 + 1.8 + Finchley.SR1 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.cloud + spring-cloud-starter-netflix-zuul + + + com.marcosbarbero.cloud + spring-cloud-zuul-ratelimit + 2.2.0.RELEASE + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/ZuulRatelimitDemoApplication.java b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/ZuulRatelimitDemoApplication.java new file mode 100644 index 0000000000..9b53099768 --- /dev/null +++ b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/ZuulRatelimitDemoApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.spring.cloud.zuulratelimitdemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.cloud.client.SpringCloudApplication; +import org.springframework.cloud.netflix.zuul.EnableZuulProxy; + +@EnableZuulProxy +@SpringCloudApplication +public class ZuulRatelimitDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(ZuulRatelimitDemoApplication.class, args); + } +} diff --git a/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingController.java b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingController.java new file mode 100644 index 0000000000..3f2ec4822f --- /dev/null +++ b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingController.java @@ -0,0 +1,21 @@ +package com.baeldung.spring.cloud.zuulratelimitdemo.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/greeting") +public class GreetingController { + + @GetMapping("/simple") + public ResponseEntity getSimple() { + return ResponseEntity.ok("Hi!"); + } + + @GetMapping("/advanced") + public ResponseEntity getAdvanced() { + return ResponseEntity.ok("Hello, how you doing?"); + } +} diff --git a/spring-cloud/spring-cloud-zuul-ratelimit/src/main/resources/application.yml b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/resources/application.yml new file mode 100644 index 0000000000..86a29ca06d --- /dev/null +++ b/spring-cloud/spring-cloud-zuul-ratelimit/src/main/resources/application.yml @@ -0,0 +1,23 @@ +zuul: + routes: + serviceSimple: + path: /greeting/simple + url: forward:/ + serviceAdvanced: + path: /greeting/advanced + url: forward:/ + ratelimit: + enabled: true + repository: JPA + policy-list: + serviceSimple: + - limit: 5 + refresh-interval: 60 + type: + - origin + serviceAdvanced: + - limit: 1 + refresh-interval: 2 + type: + - origin + strip-prefix: true \ No newline at end of file diff --git a/spring-cloud/spring-cloud-zuul-ratelimit/src/test/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingControllerTest.java b/spring-cloud/spring-cloud-zuul-ratelimit/src/test/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingControllerTest.java new file mode 100644 index 0000000000..d51f881112 --- /dev/null +++ b/spring-cloud/spring-cloud-zuul-ratelimit/src/test/java/com/baeldung/spring/cloud/zuulratelimitdemo/controller/GreetingControllerTest.java @@ -0,0 +1,112 @@ +package com.baeldung.spring.cloud.zuulratelimitdemo.controller; + +import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_LIMIT; +import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_QUOTA; +import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_REMAINING; +import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_REMAINING_QUOTA; +import static com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.RateLimitConstants.HEADER_RESET; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.http.HttpStatus.OK; +import static org.springframework.http.HttpStatus.TOO_MANY_REQUESTS; + +import java.util.concurrent.TimeUnit; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +@AutoConfigureTestDatabase +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class GreetingControllerTest { + + private static final String SIMPLE_GREETING = "/greeting/simple"; + private static final String ADVANCED_GREETING = "/greeting/advanced"; + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void whenRequestNotExceedingCapacity_thenReturnOkResponse() { + ResponseEntity response = this.restTemplate.getForEntity(SIMPLE_GREETING, String.class); + HttpHeaders headers = response.getHeaders(); + String key = "rate-limit-application_serviceSimple_127.0.0.1"; + + String limit = headers.getFirst(HEADER_LIMIT + key); + String remaining = headers.getFirst(HEADER_REMAINING + key); + String reset = headers.getFirst(HEADER_RESET + key); + + assertEquals(limit, "5"); + assertEquals(remaining, "4"); + assertEquals(reset, "60000"); + + assertEquals(OK, response.getStatusCode()); + } + + @Test + public void whenRequestExceedingCapacity_thenReturnTooManyRequestsResponse() throws InterruptedException { + ResponseEntity response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class); + HttpHeaders headers = response.getHeaders(); + String key = "rate-limit-application_serviceAdvanced_127.0.0.1"; + assertHeaders(headers, key, false, false); + assertEquals(OK, response.getStatusCode()); + + for (int i = 0; i < 2; i++) { + response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class); + } + + headers = response.getHeaders(); + String limit = headers.getFirst(HEADER_LIMIT + key); + String remaining = headers.getFirst(HEADER_REMAINING + key); + String reset = headers.getFirst(HEADER_RESET + key); + + assertEquals(limit, "1"); + assertEquals(remaining, "0"); + assertNotEquals(reset, "2000"); + + assertEquals(TOO_MANY_REQUESTS, response.getStatusCode()); + + TimeUnit.SECONDS.sleep(2); + + response = this.restTemplate.getForEntity(ADVANCED_GREETING, String.class); + headers = response.getHeaders(); + assertHeaders(headers, key, false, false); + assertEquals(OK, response.getStatusCode()); + } + + private void assertHeaders(HttpHeaders headers, String key, boolean nullable, boolean quotaHeaders) { + String quota = headers.getFirst(HEADER_QUOTA + key); + String remainingQuota = headers.getFirst(HEADER_REMAINING_QUOTA + key); + String limit = headers.getFirst(HEADER_LIMIT + key); + String remaining = headers.getFirst(HEADER_REMAINING + key); + String reset = headers.getFirst(HEADER_RESET + key); + + if (nullable) { + if (quotaHeaders) { + assertNull(quota); + assertNull(remainingQuota); + } else { + assertNull(limit); + assertNull(remaining); + } + assertNull(reset); + } else { + if (quotaHeaders) { + assertNotNull(quota); + assertNotNull(remainingQuota); + } else { + assertNotNull(limit); + assertNotNull(remaining); + } + assertNotNull(reset); + } + } +}