Commit 12ea6ed8 authored by Ashok Kumar K's avatar Ashok Kumar K

product service implementation with test cases for controller

parent cf94fc10
plugins {
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'com.nisum'
......@@ -9,27 +9,30 @@ version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
compile "io.springfox:springfox-swagger2:2.9.2"
compile "io.springfox:springfox-swagger-ui:2.9.2"
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
test {
useJUnitPlatform()
useJUnitPlatform()
}
#Thu May 28 17:36:59 IST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
......@@ -6,8 +6,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EcomProductApplication {
public static void main(String[] args) {
SpringApplication.run(EcomProductApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(EcomProductApplication.class, args);
}
}
package com.nisum.ecomproduct.controller;
import com.nisum.ecomproduct.model.Product;
import com.nisum.ecomproduct.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
@RestController
@RequestMapping("product")
@Validated
public class ProductController {
@Autowired
ProductService productService;
@PostMapping("/save")
public ResponseEntity<Product> saveProduct(@RequestBody @Valid Product product) {
return new ResponseEntity<>(productService.saveProduct(product), HttpStatus.CREATED);
}
@PostMapping("/saveall")
public ResponseEntity<List<Product>> saveProducts(@RequestBody @Valid List<Product> products) {
return new ResponseEntity<>(productService.saveProducts(products), HttpStatus.CREATED);
}
@GetMapping("/id/{id}")
public ResponseEntity<Product> getProductById(@PathVariable("id") String id) {
return ResponseEntity.ok(productService.getProductById(id));
}
@GetMapping("/search")
public ResponseEntity<List<Product>> getRelatedProducts(@RequestParam("term") String searchTerm) {
return ResponseEntity.ok(productService.getRelatedProducts(searchTerm));
}
@PutMapping("/update")
public ResponseEntity<Product> updateProduct(@RequestBody @Valid Product product) {
return ResponseEntity.ok(productService.updateProduct(product));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProductById(@PathVariable("id") String id) {
productService.deleteProductById(id);
return ResponseEntity.noContent().build();
}
}
package com.nisum.ecomproduct.exception;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.util.List;
@NoArgsConstructor
@Data
public class ErrorResponse {
HttpStatus status;
private LocalDateTime timestamp;
private String message;
private String path;
private List<SubError> subErrors;
}
\ No newline at end of file
package com.nisum.ecomproduct.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException() {
super("Product not found");
}
public ProductNotFoundException(String message) {
super(message);
}
public ProductNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
package com.nisum.ecomproduct.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@ControllerAdvice(basePackages = {"com.nisum.ecomproduct"})
public class ProductServiceExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException ex, HttpServletRequest httpServletRequest) {
return buildErrorResponseEntity(ex.getLocalizedMessage(), HttpStatus.NOT_FOUND, httpServletRequest);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest httpServletRequest) {
ErrorResponse errorResponse = buildErrorResponse("Invalid payload or params", HttpStatus.BAD_REQUEST, httpServletRequest);
List<SubError> subErrors = ex.getBindingResult().getFieldErrors()
.stream()
.map(fieldError -> new ValidationError(fieldError.getObjectName(), fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()))
.collect(Collectors.toList());
errorResponse.setSubErrors(subErrors);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Object> handleConstraintViolationException(ConstraintViolationException ex, HttpServletRequest httpServletRequest) {
ErrorResponse errorResponse = buildErrorResponse("Invalid payload or params", HttpStatus.BAD_REQUEST, httpServletRequest);
List<SubError> subErrors = ex.getConstraintViolations().stream()
.map(violation -> new ValidationError(violation.getPropertyPath().toString(), violation.getPropertyPath().toString(), violation.getInvalidValue(), violation.getMessage()))
.collect(Collectors.toList());
errorResponse.setSubErrors(subErrors);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex, HttpServletRequest httpServletRequest) {
return buildErrorResponseEntity(ex.getLocalizedMessage(), HttpStatus.INTERNAL_SERVER_ERROR, httpServletRequest);
}
private ResponseEntity<ErrorResponse> buildErrorResponseEntity(String message, HttpStatus httpstatus, HttpServletRequest httpServletRequest) {
ErrorResponse response = buildErrorResponse(message, httpstatus, httpServletRequest);
return new ResponseEntity<>(response, httpstatus);
}
private ErrorResponse buildErrorResponse(String message, HttpStatus httpstatus, HttpServletRequest servletRequest) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setStatus(httpstatus);
errorResponse.setTimestamp(LocalDateTime.now());
errorResponse.setMessage(message);
errorResponse.setPath(servletRequest.getRequestURI());
errorResponse.setSubErrors(Collections.emptyList());
return errorResponse;
}
}
package com.nisum.ecomproduct.exception;
public interface SubError {
}
package com.nisum.ecomproduct.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Data
public class ValidationError implements SubError{
private String object;
private String field;
private Object rejectedValue;
private String message;
}
package com.nisum.ecomproduct.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@NoArgsConstructor
@Data
@Document("products")
public class Product {
@Id
private String id;
@NotBlank(message = "product name can't be blank")
private String name;
@NotBlank(message = "product description can't be blank")
private String description;
@NotNull(message = "price is mandatory")
@Min(value = 1, message = "price can't be negative or zero")
private Double price;
}
package com.nisum.ecomproduct.model.dto;
public class ProductDto {
}
package com.nisum.ecomproduct.repository;
import com.nisum.ecomproduct.model.Product;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.util.List;
public interface ProductRepository extends MongoRepository<Product, String> {
List<Product> findByNameIgnoreCaseContaining(String searchTerm);
}
package com.nisum.ecomproduct.service;
import com.nisum.ecomproduct.model.Product;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface ProductService {
Product saveProduct(Product product);
List<Product> saveProducts(List<Product> products);
Product getProductById(String id);
List<Product> getRelatedProducts(String searchTerm);
Product updateProduct(Product product);
void deleteProductById(String id);
}
package com.nisum.ecomproduct.service.impl;
import com.nisum.ecomproduct.exception.ProductNotFoundException;
import com.nisum.ecomproduct.model.Product;
import com.nisum.ecomproduct.repository.ProductRepository;
import com.nisum.ecomproduct.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
ProductRepository productRepository;
@Override
public Product saveProduct(Product product) {
product.setId(null);
return productRepository.save(product);
}
@Override
public List<Product> saveProducts(List<Product> products) {
products.forEach(product -> product.setId(null));
return productRepository.saveAll(products);
}
@Override
public Product getProductById(String id) {
return productRepository.findById(id).orElseThrow(ProductNotFoundException::new);
}
@Override
public List<Product> getRelatedProducts(String searchTerm) {
return productRepository.findByNameIgnoreCaseContaining(searchTerm);
}
@Override
public Product updateProduct(Product product) {
if (product.getId() == null || !productRepository.existsById(product.getId()))
throw new ProductNotFoundException();
return productRepository.save(product);
}
@Override
public void deleteProductById(String id) {
if (productRepository.existsById(id)) productRepository.deleteById(id);
else throw new ProductNotFoundException();
}
}
spring:
data:
mongodb:
host: localhost
port: 27017
database: ecom
logging:
level:
web: debug
\ No newline at end of file
server:
port: 8011
spring:
profiles:
active: development
data:
mongodb:
host: localhost
port: 27017
database: ecom
management:
endpoints:
web:
exposure:
include: '*'
spring:
application:
name: ecom-product
\ No newline at end of file
package com.nisum.ecomproduct.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nisum.ecomproduct.exception.ProductNotFoundException;
import com.nisum.ecomproduct.model.Product;
import com.nisum.ecomproduct.service.ProductService;
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.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
ProductService productService;
@Autowired
ObjectMapper objectMapper;
private String randomId = "someHexString";
@Test
void saveProduct() throws Exception {
Product productRequestObject = getProductRequestObject();
Product productResponseObject = getProductResponseObject();
when(productService.saveProduct(any())).thenReturn(productResponseObject);
MvcResult result = mockMvc.perform(post("/product/save")
.content(objectMapper.writeValueAsString(productRequestObject))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andReturn();
Product product = objectMapper.readValue(result.getResponse().getContentAsString(), Product.class);
assertNotNull(product);
assertNotNull(product.getId());
}
@Test
void saveProducts() throws Exception {
List<Product> productsRequest = getProductRequestObjectList();
List<Product> productsResponse = getProductResponseObjectList();
when(productService.saveProducts(any())).thenReturn(productsResponse);
mockMvc.perform(post("/product/saveall")
.content(objectMapper.writeValueAsString(productsRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(3)))
.andExpect(jsonPath("$[0].id", is(randomId)))
.andExpect(jsonPath("$[1].id", is(randomId)))
.andExpect(jsonPath("$[2].id", is(randomId)))
.andExpect(status().isCreated())
.andReturn();
}
@Test
void getProductById_returnsProduct_200() throws Exception {
when(productService.getProductById(randomId)).thenReturn(getProductResponseObject());
mockMvc.perform(get("/product/id/" + randomId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(randomId)))
.andReturn();
}
@Test
void getProductById_throws_404() throws Exception {
when(productService.getProductById(randomId)).thenThrow(new ProductNotFoundException());
mockMvc.perform(get("/product/id/" + randomId))
.andExpect(status().isNotFound());
}
@Test
void getRelatedProducts() {
}
@Test
void updateProduct_returnsProduct_200() throws Exception {
Product productRequestObject = getProductRequestObject();
productRequestObject.setId(randomId);
Product productResponseObject = getProductResponseObject();
when(productService.updateProduct(any())).thenReturn(productResponseObject);
MvcResult result = mockMvc.perform(put("/product/update")
.content(objectMapper.writeValueAsString(productRequestObject))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
Product product = objectMapper.readValue(result.getResponse().getContentAsString(), Product.class);
assertNotNull(product);
assertNotNull(product.getId());
}
@Test
void updateProduct_throwsProductNotFoundException_404() throws Exception {
Product productRequestObject = getProductRequestObject();
productRequestObject.setId(randomId);
when(productService.updateProduct(any())).thenThrow(new ProductNotFoundException());
mockMvc.perform(put("/product/update")
.content(objectMapper.writeValueAsString(productRequestObject))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andReturn();
}
@Test
void deleteProductById_204() throws Exception {
doNothing().when(productService).deleteProductById(randomId);
mockMvc.perform(delete("/product/" + randomId))
.andExpect(status().isNoContent());
}
@Test
void deleteProductById_throwsProductNotFoundException_404() throws Exception {
doThrow(new ProductNotFoundException()).when(productService).deleteProductById(randomId);
mockMvc.perform(delete("/product/" + randomId))
.andExpect(status().isNotFound());
}
private Product getProductRequestObject() {
Product product = new Product();
product.setName("Dell Mouse");
product.setDescription("Input device of the computer");
product.setPrice(300D);
return product;
}
private Product getProductResponseObject() {
Product product = getProductRequestObject();
product.setId(randomId);
return product;
}
private List<Product> getProductRequestObjectList() {
Product product1 = new Product();
product1.setName("Bean bag with beans - L");
product1.setDescription("Comfortable binge");
product1.setPrice(800D);
Product product2 = new Product();
product2.setName("Bean bag with beans - XL");
product2.setDescription("Comfortable binge");
product2.setPrice(1000D);
Product product3 = new Product();
product3.setName("Bean bag with beans - XXL");
product3.setDescription("IComfortable binge");
product3.setPrice(1600D);
return Arrays.asList(product1, product2, product3);
}
private List<Product> getProductResponseObjectList() {
List<Product> products = getProductRequestObjectList();
products.forEach(product -> product.setId(randomId));
return products;
}
}
\ 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