Skip to content

Commit b775f8f

Browse files
committed
Validate Create Order Request
Implemented OrderValidator to call catalog-service and check product code and price are valid Used resilience4j to apply circuit breaker and retry Used timeout configuration with RestClient Used WireMock for mocking catalog-service integration for testing
1 parent f9298c9 commit b775f8f

File tree

12 files changed

+212
-42
lines changed

12 files changed

+212
-42
lines changed

order-service/pom.xml

+17-14
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@
1515
<description>Order Service</description>
1616
<properties>
1717
<java.version>21</java.version>
18-
<spring-cloud.version>2023.0.1</spring-cloud.version>
1918
<springdoc-openapi.version>2.5.0</springdoc-openapi.version>
2019
<instancio.version>4.5.1</instancio.version>
2120
<spotless.version>2.43.0</spotless.version>
21+
<resilience4j-spring-boot3.version>2.2.0</resilience4j-spring-boot3.version>
2222
<dockerImageName>kavinduperera/bookstore-${project.artifactId}</dockerImageName>
2323
</properties>
2424
<dependencies>
@@ -46,9 +46,11 @@
4646
<groupId>org.flywaydb</groupId>
4747
<artifactId>flyway-core</artifactId>
4848
</dependency>
49+
4950
<dependency>
50-
<groupId>org.springframework.cloud</groupId>
51-
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
51+
<groupId>io.github.resilience4j</groupId>
52+
<artifactId>resilience4j-spring-boot3</artifactId>
53+
<version>${resilience4j-spring-boot3.version}</version>
5254
</dependency>
5355
<dependency>
5456
<groupId>org.springdoc</groupId>
@@ -118,18 +120,19 @@
118120
<version>${instancio.version}</version>
119121
<scope>test</scope>
120122
</dependency>
123+
<dependency>
124+
<groupId>org.wiremock</groupId>
125+
<artifactId>wiremock-standalone</artifactId>
126+
<version>3.5.2</version>
127+
<scope>test</scope>
128+
</dependency>
129+
<dependency>
130+
<groupId>org.wiremock.integrations.testcontainers</groupId>
131+
<artifactId>wiremock-testcontainers-module</artifactId>
132+
<version>1.0-alpha-13</version>
133+
<scope>test</scope>
134+
</dependency>
121135
</dependencies>
122-
<dependencyManagement>
123-
<dependencies>
124-
<dependency>
125-
<groupId>org.springframework.cloud</groupId>
126-
<artifactId>spring-cloud-dependencies</artifactId>
127-
<version>${spring-cloud.version}</version>
128-
<type>pom</type>
129-
<scope>import</scope>
130-
</dependency>
131-
</dependencies>
132-
</dependencyManagement>
133136

134137
<build>
135138
<plugins>

order-service/src/main/java/com/codebykavindu/bookstore/orders/ApplicationProperties.java

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
@ConfigurationProperties(prefix = "orders")
99
public record ApplicationProperties(
10+
String catalogServiceUrl,
1011
String orderEventsExchange,
1112
String newOrdersQueue,
1213
String deliveredOrdersQueue,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.codebykavindu.bookstore.orders.clients.catalog;
2+
3+
import com.codebykavindu.bookstore.orders.ApplicationProperties;
4+
import java.time.Duration;
5+
import org.springframework.boot.web.client.ClientHttpRequestFactories;
6+
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.web.client.RestClient;
10+
11+
/**
12+
* @author Kavindu Perera
13+
*/
14+
@Configuration
15+
class CatalogServiceClientConfig {
16+
17+
@Bean
18+
RestClient restClient(ApplicationProperties properties) {
19+
return RestClient.builder()
20+
.baseUrl(properties.catalogServiceUrl())
21+
.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
22+
.withConnectTimeout(Duration.ofSeconds(5))
23+
.withReadTimeout(Duration.ofSeconds(5))))
24+
.build();
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.codebykavindu.bookstore.orders.clients.catalog;
2+
3+
import java.math.BigDecimal;
4+
5+
/**
6+
* @author Kavindu Perera
7+
*/
8+
public record Product(String code, String name, String description, String imageUrl, BigDecimal price) {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.codebykavindu.bookstore.orders.clients.catalog;
2+
3+
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
4+
import io.github.resilience4j.retry.annotation.Retry;
5+
import java.util.Optional;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.web.client.RestClient;
10+
11+
/**
12+
* @author Kavindu Perera
13+
*/
14+
@Component
15+
public class ProductServiceClient {
16+
private static final Logger log = LoggerFactory.getLogger(ProductServiceClient.class);
17+
18+
private final RestClient restClient;
19+
20+
ProductServiceClient(RestClient restClient) {
21+
this.restClient = restClient;
22+
}
23+
24+
@CircuitBreaker(name = "catalog-service")
25+
@Retry(name = "catalog-service", fallbackMethod = "getProductByCodeFallback")
26+
public Optional<Product> getProductByCode(String code) {
27+
log.info("Fetching product with code: {}", code);
28+
var product =
29+
restClient.get().uri("api/v1/products/{code}", code).retrieve().body(Product.class);
30+
return Optional.ofNullable(product);
31+
}
32+
33+
Optional<Product> getProductByCodeFallback(String code, Throwable t) {
34+
log.error("ProductServiceClient.getProductByCodeFallback: code {}", code);
35+
return Optional.empty();
36+
}
37+
}

order-service/src/main/java/com/codebykavindu/bookstore/orders/domain/OrderService.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ public class OrderService {
1616
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
1717

1818
private final OrderRepository orderRepository;
19+
private final OrderValidator orderValidator;
1920

20-
OrderService(OrderRepository orderRepository) {
21+
OrderService(OrderRepository orderRepository, OrderValidator orderValidator) {
2122
this.orderRepository = orderRepository;
23+
this.orderValidator = orderValidator;
2224
}
2325

2426
public CreateOrderResponse createOrder(String userName, CreateOrderRequest request) {
27+
orderValidator.validate(request);
2528
OrderEntity newOrder = OrderMapper.convertToEntity(request);
2629
newOrder.setUserName(userName);
2730
OrderEntity savedOrder = this.orderRepository.save(newOrder);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.codebykavindu.bookstore.orders.domain;
2+
3+
import com.codebykavindu.bookstore.orders.clients.catalog.Product;
4+
import com.codebykavindu.bookstore.orders.clients.catalog.ProductServiceClient;
5+
import com.codebykavindu.bookstore.orders.domain.models.CreateOrderRequest;
6+
import com.codebykavindu.bookstore.orders.domain.models.OrderItem;
7+
import java.util.Set;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import org.springframework.stereotype.Component;
11+
12+
/**
13+
* @author Kavindu Perera
14+
*/
15+
@Component
16+
class OrderValidator {
17+
private static final Logger log = LoggerFactory.getLogger(OrderValidator.class);
18+
19+
private final ProductServiceClient client;
20+
21+
OrderValidator(ProductServiceClient client) {
22+
this.client = client;
23+
}
24+
25+
void validate(CreateOrderRequest request) {
26+
Set<OrderItem> items = request.items();
27+
for (OrderItem item : items) {
28+
Product product = client.getProductByCode(item.code())
29+
.orElseThrow(() -> new InvalidOrderException("Invalid Product code: " + item.code()));
30+
if (item.price().compareTo(product.price()) != 0) {
31+
log.error(
32+
"Product price not matching for. Actual price:{}, received price:{}",
33+
product.price(),
34+
item.price());
35+
throw new InvalidOrderException("Product price not matching");
36+
}
37+
}
38+
}
39+
}

order-service/src/main/java/com/codebykavindu/bookstore/orders/web/controllers/OrderController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class OrderController {
2525
private final OrderService orderService;
2626
private final SecurityService securityService;
2727

28-
public OrderController(OrderService orderService, SecurityService securityService) {
28+
OrderController(OrderService orderService, SecurityService securityService) {
2929
this.orderService = orderService;
3030
this.securityService = securityService;
3131
}

order-service/src/main/java/com/codebykavindu/bookstore/orders/web/controllers/RabbitMQController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class RabbitMQController {
1414
private final RabbitTemplate rabbitTemplate;
1515
private final ApplicationProperties properties;
1616

17-
public RabbitMQController(RabbitTemplate rabbitTemplate, ApplicationProperties properties) {
17+
RabbitMQController(RabbitTemplate rabbitTemplate, ApplicationProperties properties) {
1818
this.rabbitTemplate = rabbitTemplate;
1919
this.properties = properties;
2020
}

order-service/src/main/resources/application.properties

+13-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ management.endpoints.web.exposure.include=*
55
management.info.git.mode=full
66

77
# Order Service Configurations
8+
orders.catalog-service-url=http://localhost:8081
89
orders.order-events-exchange=orders-exchange
910
orders.new-orders-queue=new-orders
1011
orders.delivered-orders-queue=delivered-orders
@@ -21,4 +22,15 @@ spring.jpa.open-in-view=false
2122
spring.rabbitmq.host=${RABBITMQ_HOST:localhost}
2223
spring.rabbitmq.port=${RABBITMQ_PORT:5672}
2324
spring.rabbitmq.username=${RABBITMQ_USERNAME:guest}
24-
spring.rabbitmq.password=${RABBITMQ_PASSWORD:guest}
25+
spring.rabbitmq.password=${RABBITMQ_PASSWORD:guest}
26+
27+
# Resilience4j Configurations
28+
resilience4j.retry.backends.catalog-service.max-attempts=2
29+
resilience4j.retry.backends.catalog-service.wait-duration=1s
30+
31+
resilience4j.circuitbreaker.backends.catalog-service.sliding-window-type=COUNT_BASED
32+
resilience4j.circuitbreaker.backends.catalog-service.sliding-window-size=6
33+
resilience4j.circuitbreaker.backends.catalog-service.minimum-number-of-calls=4
34+
resilience4j.circuitbreaker.backends.catalog-service.wait-duration-in-open-state=20s
35+
resilience4j.circuitbreaker.backends.catalog-service.permitted-number-of-calls-in-half-open-state=2
36+
resilience4j.circuitbreaker.backends.catalog-service.failure-rate-threshold=50

order-service/src/test/java/com/codebykavindu/bookstore/orders/AbstractIntegrationTest.java

+40
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
package com.codebykavindu.bookstore.orders;
22

3+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
4+
import static com.github.tomakehurst.wiremock.client.WireMock.configureFor;
5+
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
6+
import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
37
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
48

9+
import com.github.tomakehurst.wiremock.client.WireMock;
510
import io.restassured.RestAssured;
11+
import java.math.BigDecimal;
12+
import org.junit.jupiter.api.BeforeAll;
613
import org.junit.jupiter.api.BeforeEach;
714
import org.springframework.boot.test.context.SpringBootTest;
815
import org.springframework.boot.test.web.server.LocalServerPort;
916
import org.springframework.context.annotation.Import;
17+
import org.springframework.http.MediaType;
18+
import org.springframework.test.context.DynamicPropertyRegistry;
19+
import org.springframework.test.context.DynamicPropertySource;
20+
import org.wiremock.integrations.testcontainers.WireMockContainer;
1021

1122
/**
1223
* @author Kavindu Perera
@@ -17,8 +28,37 @@ public abstract class AbstractIntegrationTest {
1728
@LocalServerPort
1829
int port;
1930

31+
static WireMockContainer wiremockServer = new WireMockContainer("wiremock/wiremock:3.5.2-alpine");
32+
33+
@BeforeAll
34+
static void beforeAll() {
35+
wiremockServer.start();
36+
configureFor(wiremockServer.getHost(), wiremockServer.getPort());
37+
}
38+
39+
@DynamicPropertySource
40+
static void configureProperties(DynamicPropertyRegistry registry) {
41+
registry.add("orders.catalog-service-url", wiremockServer::getBaseUrl);
42+
}
43+
2044
@BeforeEach
2145
void setUp() {
2246
RestAssured.port = port;
2347
}
48+
49+
protected static void mockGetProductByCode(String code, String name, BigDecimal price) {
50+
stubFor(WireMock.get(urlMatching("/api/v1/products/" + code))
51+
.willReturn(aResponse()
52+
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
53+
.withStatus(200)
54+
.withBody(
55+
"""
56+
{
57+
"code": "%s",
58+
"name": "%s",
59+
"price": %f
60+
}
61+
"""
62+
.formatted(code, name, price.doubleValue()))));
63+
}
2464
}

order-service/src/test/java/com/codebykavindu/bookstore/orders/web/controllers/OrderControllerTests.java

+25-24
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.codebykavindu.bookstore.orders.AbstractIntegrationTest;
77
import com.codebykavindu.bookstore.orders.testdata.TestDataFactory;
88
import io.restassured.http.ContentType;
9+
import java.math.BigDecimal;
910
import org.junit.jupiter.api.Nested;
1011
import org.junit.jupiter.api.Test;
1112
import org.springframework.http.HttpStatus;
@@ -19,33 +20,33 @@ class OrderControllerTests extends AbstractIntegrationTest {
1920
class CreateOrderTests {
2021
@Test
2122
void shouldCreateOrderSuccessfully() {
23+
mockGetProductByCode("P100", "Product 1", new BigDecimal("25.50"));
2224
var payload =
2325
"""
24-
{
25-
"customer" : {
26-
"name": "Kavindu",
27-
"email": "kavindu@gmail.com",
28-
"phone": "999999999"
29-
},
30-
"deliveryAddress" : {
31-
"addressLine1": "Birkelweg",
32-
"addressLine2": "Hans-Edenhofer-Straße 23",
33-
"city": "Berlin",
34-
"state": "Berlin",
35-
"zipCode": "94258",
36-
"country": "Germany"
37-
},
38-
"items": [
39-
{
40-
"code": "P100",
41-
"name": "Product 1",
42-
"price": 25.50,
43-
"quantity": 1
44-
}
45-
]
46-
}
26+
{
27+
"customer" : {
28+
"name": "Siva",
29+
"email": "siva@gmail.com",
30+
"phone": "999999999"
31+
},
32+
"deliveryAddress" : {
33+
"addressLine1": "HNO 123",
34+
"addressLine2": "Kukatpally",
35+
"city": "Hyderabad",
36+
"state": "Telangana",
37+
"zipCode": "500072",
38+
"country": "India"
39+
},
40+
"items": [
41+
{
42+
"code": "P100",
43+
"name": "Product 1",
44+
"price": 25.50,
45+
"quantity": 1
46+
}
47+
]
48+
}
4749
""";
48-
4950
given().contentType(ContentType.JSON)
5051
.body(payload)
5152
.when()

0 commit comments

Comments
 (0)