Commit 6fb723dc authored by Ashok Kumar K's avatar Ashok Kumar K

added place order endpoint with tests and included Spring Cloud Microservices

parent f88fdfe4
......@@ -30,6 +30,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-hystrix'
compile 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
compileOnly 'org.projectlombok:lombok'
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
......
......@@ -2,7 +2,9 @@ package com.nisum.ecomorder;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
......@@ -10,7 +12,9 @@ import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableMongoAuditing
@EnableEurekaClient
@EnableFeignClients
@EnableCircuitBreaker
public class EcomOrderApplication {
public static void main(String[] args) {
......
package com.nisum.ecomorder.client;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.nisum.ecomorder.exception.ClientException;
import com.nisum.ecomorder.exception.NotFoundException;
import com.nisum.ecomorder.model.Customer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpClientErrorException.NotFound;
import org.springframework.web.client.RestTemplate;
@Component
......@@ -15,8 +20,19 @@ public class CustomerClient {
private final String baseUrl = "http://ecom-customer/";
public Customer getCustomerById(String id) {
@HystrixCommand(fallbackMethod = "getCustomerByIdFallback")
public Customer getCustomerById(Long id) {
return restTemplate.getForEntity(baseUrl + "id/" + id, Customer.class).getBody();
}
public Customer getCustomerByIdFallback(Long id, Throwable e) {
log.error("Error while calling ecom-product#getCustomerById with id:: {} -- cause :: {}", id, e.getMessage());
if (e instanceof IllegalStateException) throw new ClientException();
if (e instanceof HttpClientErrorException) {
if (e instanceof NotFound) throw new NotFoundException("customer not found with ID: " + id);
else throw new ClientException();
}
return null;
}
}
......@@ -5,9 +5,11 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name = "ecom-product", decode404 = true)
@FeignClient(name = "ecom-product", decode404 = true, fallbackFactory = ProductClientFallbackFactory.class)
public interface ProductClient {
@GetMapping(value = "/id/{id}")
Product getProductById(@PathVariable("id") String id);
}
package com.nisum.ecomorder.client;
import com.nisum.ecomorder.model.Product;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ProductClientFallback implements ProductClient {
private final Throwable cause;
public ProductClientFallback(Throwable cause) {
this.cause = cause;
}
@Override
public Product getProductById(String id) {
log.error("Error from product client:: {}", cause.getMessage());
// TODO - check why fallback is not being called
return new Product();
}
}
package com.nisum.ecomorder.client;
import feign.hystrix.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class ProductClientFallbackFactory implements FallbackFactory<ProductClient> {
@Override
public ProductClient create(Throwable cause) {
return new ProductClientFallback(cause);
}
}
\ No newline at end of file
......@@ -4,15 +4,16 @@ import com.nisum.ecomorder.model.Order;
import com.nisum.ecomorder.model.dto.PlaceOrderRequestDto;
import com.nisum.ecomorder.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
@RefreshScope
@RestController
public class OrderController {
......@@ -21,7 +22,12 @@ public class OrderController {
@PostMapping("/place-order")
public ResponseEntity<Order> placeOrder(@RequestBody @Valid PlaceOrderRequestDto placeOrderRequestDto) {
return ResponseEntity.ok(orderService.placeOrder(placeOrderRequestDto));
return new ResponseEntity<>(orderService.placeOrder(placeOrderRequestDto), HttpStatus.CREATED);
}
@GetMapping("/all")
public ResponseEntity<List<Order>> getAllOrders() {
return ResponseEntity.ok(orderService.getAllOrders());
}
......
package com.nisum.ecomorder.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.springframework.http.HttpStatus;
@Data
public class ClientError implements SubError{
public class ClientError implements SubError {
private HttpStatus status;
private String message;
}
package com.nisum.ecomorder.exception;
public class ClientException extends RuntimeException {
public ClientException() {
super();
}
public ClientException(String message) {
super(message);
}
public ClientException(String message, Throwable cause) {
super(message, cause);
}
}
package com.nisum.ecomorder.exception;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException;
import lombok.extern.slf4j.Slf4j;
......@@ -58,19 +57,22 @@ public class OrderServiceExceptionHandler {
}
@ExceptionHandler(FeignException.class)
public ResponseEntity<ErrorResponse> handleFeignException(FeignException ex, HttpServletRequest httpServletRequest) throws JsonProcessingException {
String responseBodyString = ex.getMessage();
log.error(responseBodyString);
return new ResponseEntity<>(objectMapper.readValue(responseBodyString, ErrorResponse.class), HttpStatus.valueOf(ex.status()));
public ResponseEntity<ErrorResponse> handleFeignException(FeignException ex, HttpServletRequest httpServletRequest) {
int statusCode = ex.status() == -1 ? 500 : ex.status();
return buildErrorResponseEntity(ex.getMessage(), HttpStatus.valueOf(statusCode), httpServletRequest);
}
@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<ErrorResponse> handleHttpClientErrorException(HttpClientErrorException ex, HttpServletRequest httpServletRequest) throws JsonProcessingException {
public ResponseEntity<ErrorResponse> handleHttpClientErrorException(HttpClientErrorException ex, HttpServletRequest httpServletRequest) {
ErrorResponse errorResponse = buildErrorResponse(ex.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR, httpServletRequest);
// errorResponse.getSubErrors().add(objectMapper.readValue(ex.getMessage(), ClientError.class));
return new ResponseEntity<>(errorResponse, ex.getStatusCode());
}
@ExceptionHandler(ClientException.class)
public ResponseEntity<ErrorResponse> handleClientException(ClientException ex, HttpServletRequest httpServletRequest) {
return buildErrorResponseEntity(ex.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR, httpServletRequest);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex, HttpServletRequest httpServletRequest) {
return buildErrorResponseEntity(ex.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR, httpServletRequest);
......
package com.nisum.ecomorder.model;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Setter;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Setter(value = AccessLevel.PRIVATE)
public class Customer {
private String id;
private Long id;
private String firstName;
private String lastName;
private String email;
......
......@@ -23,7 +23,7 @@ public class Order {
@Setter(value = AccessLevel.PRIVATE)
private LocalDateTime orderDate;
private String customerId;
private Long customerId;
private String customerName;
......
package com.nisum.ecomorder.model;
import lombok.Data;
import lombok.*;
@NoArgsConstructor
@AllArgsConstructor
@Data
@Setter(value = AccessLevel.PRIVATE)
public class Product {
private String id;
private String name;
......
package com.nisum.ecomorder.model.dto;
public class OrderResponseDto {
}
......@@ -3,11 +3,9 @@ package com.nisum.ecomorder.model.dto;
import com.nisum.ecomorder.model.Address;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.List;
......@@ -16,8 +14,8 @@ import java.util.List;
@Data
public class PlaceOrderRequestDto {
@NotBlank(message = "missing customer id")
private String customerId;
@NotNull(message = "missing customer id")
private Long customerId;
@Email
private String email;
......
......@@ -4,8 +4,12 @@ import com.nisum.ecomorder.model.Order;
import com.nisum.ecomorder.model.dto.PlaceOrderRequestDto;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface OrderService {
Order placeOrder(PlaceOrderRequestDto placeOrderRequestDto);
List<Order> getAllOrders();
}
......@@ -2,6 +2,7 @@ package com.nisum.ecomorder.service.impl;
import com.nisum.ecomorder.client.CustomerClient;
import com.nisum.ecomorder.client.ProductClient;
import com.nisum.ecomorder.exception.ClientException;
import com.nisum.ecomorder.exception.NotFoundException;
import com.nisum.ecomorder.model.Customer;
import com.nisum.ecomorder.model.Order;
......@@ -14,7 +15,8 @@ import com.nisum.ecomorder.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import java.util.List;
@Service
@Slf4j
......@@ -39,9 +41,10 @@ public class OrderServiceImpl implements OrderService {
try {
Customer customer = customerClient.getCustomerById(order.getCustomerId());
order.setCustomerName(customer.getFirstName() + " " + customer.getLastName());
} catch (HttpClientErrorException.NotFound ex) {
throw new NotFoundException("customer not found with ID: " + order.getCustomerId());
} catch (ClientException e) {
throw new ClientException("Order can't be placed due to internal errors. Please try later");
}
order.getLineItems().forEach(orderLine -> {
orderLine.setId(null);
String productId = orderLine.getProductId();
......@@ -54,4 +57,9 @@ public class OrderServiceImpl implements OrderService {
order.setOrderTotal(order.getLineItems().stream().mapToDouble(OrderLine::getSubTotal).sum());
return orderRepository.save(order);
}
@Override
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
}
logging:
level:
web: debug
com.nisum.ecomorder.client.ProductClient: debug
\ No newline at end of file
com.nisum.ecomorder.client.ProductClient: debug
feign:
hystix:
enabled: true
spring:
data:
mongodb:
host: localhost
port: 27017
database: ecom-test
cloud:
config:
discovery:
enabled: false
logging:
level:
web: debug
#disable eureka from using eureka naming service
ribbon:
eureka:
enabled: false
#providing list of servers to Ribbon based on service name explicitly so that these can be used in mocking for integration test
ecom-product:
ribbon:
listOfServers: http://localhost:8088
ecom-customer:
ribbon:
listOfServers: http://localhost:8099
\ No newline at end of file
package com.nisum.ecomorder.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.nisum.ecomorder.client.CustomerClient;
import com.nisum.ecomorder.client.ProductClient;
import com.nisum.ecomorder.model.Address;
import com.nisum.ecomorder.model.Customer;
import com.nisum.ecomorder.model.Order;
import com.nisum.ecomorder.model.Product;
import com.nisum.ecomorder.model.dto.OrderLineDto;
import com.nisum.ecomorder.model.dto.PlaceOrderRequestDto;
import com.nisum.ecomorder.repository.OrderRepository;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.contract.wiremock.AutoConfigureHttpClient;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ActiveProfiles;
import java.util.Arrays;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestInstance(Lifecycle.PER_CLASS)
@ActiveProfiles("test")
@AutoConfigureHttpClient
public class OrderControllerIntegrationTest {
@LocalServerPort
private int port;
private String baseUrl;
private final String randomId = "someHexString";
private final WireMockServer productMockServer = new WireMockServer(8088);
private final WireMockServer customerMockServer = new WireMockServer(8099);
@Autowired
OrderRepository orderRepository;
@Autowired
private TestRestTemplate restTemplate;
@Autowired
ObjectMapper objectMapper;
@Autowired
ProductClient productClient;
@Autowired
CustomerClient customerClient;
@BeforeAll
public void setUp() {
initializeDb();
baseUrl = "http://localhost:" + port;
productMockServer.start();
customerMockServer.start();
}
@AfterEach
void after() {
productMockServer.resetAll();
customerMockServer.resetAll();
}
@AfterAll
void clean() {
productMockServer.shutdown();
customerMockServer.shutdown();
}
@Test
void testProductServer() throws JsonProcessingException {
createStubForGetProductById();
Product productById = productClient.getProductById(randomId);
assertEquals(randomId, productById.getId());
assertEquals("Bean bag", productById.getName());
}
@Test
void testCustomerServer() throws JsonProcessingException {
createStubForGetCustomerById();
Customer customerById = customerClient.getCustomerById(123L);
assertEquals(123L, customerById.getId());
assertEquals("Ashok", customerById.getFirstName());
}
@Test
void placeOrder_success() throws JsonProcessingException {
PlaceOrderRequestDto placeOrderRequestObject = getPlaceOrderRequestObject();
createStubForGetCustomerById();
createStubForGetProductById();
ResponseEntity<Order> orderResponseEntity = this.restTemplate.postForEntity(baseUrl + "/place-order", placeOrderRequestObject, Order.class);
assertEquals(HttpStatus.CREATED, orderResponseEntity.getStatusCode());
Order orderResponse = orderResponseEntity.getBody();
System.out.println(orderResponse);
assertNotNull(orderResponse);
assertNotNull(orderResponse.getId());
assertEquals("Ashok Kumar", orderResponse.getCustomerName());
orderResponse.getLineItems().forEach(orderLine -> assertNotNull(orderLine.getId()));
assertEquals(4800D, orderResponse.getOrderTotal());
}
private void initializeDb() {
orderRepository.deleteAll();
}
private void createStubForGetProductById() throws JsonProcessingException {
Product product = new Product(randomId, "Bean bag", "test description", 1200D);
String responseJson = objectMapper.writeValueAsString(product);
productMockServer.stubFor(
get(urlEqualTo("/id/" + randomId))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withStatus(200)
.withBody(responseJson)
)
);
}
private void createStubForGetCustomerById() throws JsonProcessingException {
Customer customer = new Customer(123L, "Ashok", "Kumar", "ashokk@nisum.com", "9999999988");
String responseJson = objectMapper.writeValueAsString(customer);
customerMockServer.stubFor(
get(urlEqualTo("/id/123"))
.willReturn(
aResponse()
.withHeader("Content-Type", "application/json")
.withStatus(200)
.withBody(responseJson)
)
);
}
private PlaceOrderRequestDto getPlaceOrderRequestObject() {
PlaceOrderRequestDto placeOrderRequestDto = new PlaceOrderRequestDto();
placeOrderRequestDto.setCustomerId(123L);
placeOrderRequestDto.setEmail("ashokk@nisum.com");
Address shippingAddress = new Address();
shippingAddress.setAddressLine1("Jawaharnagar colony");
shippingAddress.setAddressLine2("Ameerpet");
shippingAddress.setCity("Hyderabad");
shippingAddress.setState("TS");
shippingAddress.setCountry("IND");
shippingAddress.setZipCode("500001");
placeOrderRequestDto.setShipTo(shippingAddress);
OrderLineDto orderLineDto1 = new OrderLineDto();
orderLineDto1.setProductId(randomId);
orderLineDto1.setQuantity(2);
OrderLineDto orderLineDto2 = new OrderLineDto();
orderLineDto2.setProductId(randomId);
orderLineDto2.setQuantity(2);
placeOrderRequestDto.setLineItems(Arrays.asList(orderLineDto1, orderLineDto2));
return placeOrderRequestDto;
}
}
package com.nisum.ecomorder.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nisum.ecomorder.model.Address;
import com.nisum.ecomorder.model.Order;
import com.nisum.ecomorder.model.dto.OrderLineDto;
import com.nisum.ecomorder.model.dto.PlaceOrderRequestDto;
import com.nisum.ecomorder.model.mapper.OrderLineMapperImpl;
import com.nisum.ecomorder.model.mapper.OrderMapper;
import com.nisum.ecomorder.model.mapper.OrderMapperImpl;
import com.nisum.ecomorder.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(value = {OrderController.class})
@ActiveProfiles("test")
@ContextConfiguration(classes = {OrderController.class, OrderMapperImpl.class, OrderLineMapperImpl.class})
class OrderControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
OrderService orderService;
@Autowired
ObjectMapper objectMapper;
@Autowired
OrderMapper orderMapper;
private final String randomId = "someHexString";
@Test
void placeOrder() throws Exception {
PlaceOrderRequestDto placeOrderRequestObject = getPlaceOrderRequestObject();
Order placeOrderResponseObject = getPlaceOrderResponseObject();
when(orderService.placeOrder(placeOrderRequestObject)).thenReturn(placeOrderResponseObject);
String requestJson = objectMapper.writeValueAsString(placeOrderRequestObject);
MvcResult mvcResult = mockMvc.perform(post("/place-order")
.content(requestJson)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andReturn();
Order orderResponse = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), Order.class);
assertNotNull(orderResponse);
assertNotNull(orderResponse.getId());
orderResponse.getLineItems().forEach(orderLine -> assertNotNull(orderLine.getId()));
}
private PlaceOrderRequestDto getPlaceOrderRequestObject() {
PlaceOrderRequestDto placeOrderRequestDto = new PlaceOrderRequestDto();
placeOrderRequestDto.setCustomerId(2L);
placeOrderRequestDto.setEmail("ashokk@nisum.com");
Address shippingAddress = new Address();
shippingAddress.setAddressLine1("Jawaharnagar colony");
shippingAddress.setAddressLine2("Ameerpet");
shippingAddress.setCity("Hyderabad");
shippingAddress.setState("TS");
shippingAddress.setCountry("IND");
shippingAddress.setZipCode("500001");
placeOrderRequestDto.setShipTo(shippingAddress);
OrderLineDto orderLineDto1 = new OrderLineDto();
orderLineDto1.setProductId(randomId);
orderLineDto1.setQuantity(2);
OrderLineDto orderLineDto2 = new OrderLineDto();
orderLineDto2.setProductId(randomId);
orderLineDto2.setQuantity(2);
placeOrderRequestDto.setLineItems(Arrays.asList(orderLineDto1, orderLineDto2));
return placeOrderRequestDto;
}
private Order getPlaceOrderResponseObject() {
Order order = orderMapper.placeOrderRequestDtoToOrder(getPlaceOrderRequestObject());
order.setId(randomId);
// order.setOrderDate(LocalDateTime.now());
order.setCustomerName("Ashok kumar");
order.getLineItems().forEach(orderLine -> {
orderLine.setId(randomId);
orderLine.setProductName("new bean bag");
orderLine.setPrice(1250d);
orderLine.setSubTotal(2500d);
});
order.setOrderTotal(5000D);
return order;
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment