diff --git a/src/main/java/com/nisum/reactiveweb/controller/ProductHandler.java b/src/main/java/com/nisum/reactiveweb/controller/ProductHandler.java index 7ede2e419b5aa1c9a2c2a66abf7082673eb26b7a..f27ec95a1b21cb7c8c4434108a1e852422dfc623 100644 --- a/src/main/java/com/nisum/reactiveweb/controller/ProductHandler.java +++ b/src/main/java/com/nisum/reactiveweb/controller/ProductHandler.java @@ -8,8 +8,8 @@ import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.support.DefaultMessageSourceResolvable; -import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; @@ -24,7 +24,7 @@ import static org.springframework.web.reactive.function.server.ServerResponse.cr import static org.springframework.web.reactive.function.server.ServerResponse.ok; @Slf4j -@Component +@Configuration public class ProductHandler { @Autowired @@ -57,7 +57,6 @@ public class ProductHandler { .flatMap(product -> productService.updateProduct(req.pathVariable(SKU_TEXT), product)) .flatMap(product -> accepted() .bodyValue(product)) - ) .DELETE(SKU_TEXT_PATH_VAR, req -> productService.removeProduct(req.pathVariable(SKU_TEXT)) diff --git a/src/main/java/com/nisum/reactiveweb/exceptions/ApiError.java b/src/main/java/com/nisum/reactiveweb/exceptions/ApiErrorAttributes.java similarity index 96% rename from src/main/java/com/nisum/reactiveweb/exceptions/ApiError.java rename to src/main/java/com/nisum/reactiveweb/exceptions/ApiErrorAttributes.java index 2cbb476d9f9d8100d07154ef9fb368c8bf5e15bc..ddede4875d6b6e70b703f47543f630c2b57026de 100644 --- a/src/main/java/com/nisum/reactiveweb/exceptions/ApiError.java +++ b/src/main/java/com/nisum/reactiveweb/exceptions/ApiErrorAttributes.java @@ -17,7 +17,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND; @Component @Slf4j -public class ApiError extends DefaultErrorAttributes { +public class ApiErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { diff --git a/src/main/java/com/nisum/reactiveweb/repository/ProductRepository.java b/src/main/java/com/nisum/reactiveweb/repository/ProductRepository.java index b936ddaeb43da811efb892c898129fbc8f5c2135..2e7e18e0ca84b2aa92bdd1374db62cc9d8ef0d30 100644 --- a/src/main/java/com/nisum/reactiveweb/repository/ProductRepository.java +++ b/src/main/java/com/nisum/reactiveweb/repository/ProductRepository.java @@ -14,4 +14,6 @@ public interface ProductRepository { Mono<Product> updateProduct(String sku, Product product); Mono<Long> removeProduct(String sku); + + Mono<Long> removeAllProducts(); } diff --git a/src/main/java/com/nisum/reactiveweb/repository/ProductRepositoryImpl.java b/src/main/java/com/nisum/reactiveweb/repository/ProductRepositoryImpl.java index af0384bfeebc35711fd13b9de94746867ccd32a6..2f45e9870638f3c96ae6fb423b0b6d966b2db8af 100644 --- a/src/main/java/com/nisum/reactiveweb/repository/ProductRepositoryImpl.java +++ b/src/main/java/com/nisum/reactiveweb/repository/ProductRepositoryImpl.java @@ -54,4 +54,9 @@ public class ProductRepositoryImpl implements ProductRepository { .map(DeleteResult::getDeletedCount); } + @Override + public Mono<Long> removeAllProducts() { + return template.remove(new Query(), Product.class) + .map(DeleteResult::getDeletedCount); + } } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..3345aff2b85617dd13b468bc882cd4a28518b5e5 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,6 @@ +spring: + data: + mongodb: + host: localhost + port: 27017 + database: reactive-test \ No newline at end of file diff --git a/src/test/java/com/nisum/reactiveweb/ReactiveWebApplicationTests.java b/src/test/java/com/nisum/reactiveweb/ReactiveWebApplicationTests.java index e664cb12d8ce29829974c88de4236f8142d5d3e9..9d749439b1f4aa1a19921629e24f0344613fc514 100644 --- a/src/test/java/com/nisum/reactiveweb/ReactiveWebApplicationTests.java +++ b/src/test/java/com/nisum/reactiveweb/ReactiveWebApplicationTests.java @@ -1,13 +1,7 @@ package com.nisum.reactiveweb; -import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ReactiveWebApplicationTests { - - @Test - void contextLoads() { - } - } diff --git a/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerComponentTests.java b/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerComponentTests.java new file mode 100644 index 0000000000000000000000000000000000000000..78ecb7b04025f699937e73885709d2388d1edfd8 --- /dev/null +++ b/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerComponentTests.java @@ -0,0 +1,189 @@ +package com.nisum.reactiveweb.controller; + +import com.nisum.reactiveweb.exceptions.ApiErrorAttributes; +import com.nisum.reactiveweb.exceptions.ProductNotFoundException; +import com.nisum.reactiveweb.model.Product; +import com.nisum.reactiveweb.service.ProductServiceImpl; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; +import java.util.function.Predicate; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; +import static reactor.core.publisher.Mono.just; + +@WebFluxTest() +@Import({ProductHandler.class, ApiErrorAttributes.class}) +class ProductHandlerComponentTests { + + @Autowired + private WebTestClient testClient; + + @MockBean + private ProductServiceImpl productService; + + private final Product product1 = new Product("1", "toy", new BigDecimal(200)); + + private final Product product3 = new Product("1", "toy", new BigDecimal(300)); + + private final Product product2 = new Product("2", "table", new BigDecimal(400)); + + private final Predicate<Product> productPredicate1 = productPredicate("1", "toy", 200); + + private final Predicate<Product> productPredicate2 = productPredicate("1", "toy", 300); + + private final Mono<Product> productNotFoundErrorMono = Mono.error(ProductNotFoundException::new); + + @Test + void getProductBySku() { + when(productService.getProductBySku("1")).thenReturn(productMono()); + testClient.get() + .uri("/product/1") + .exchange() + .expectStatus().isOk() + .expectBody(Product.class).value(Matchers.equalTo(product1)); + } + + @Test + void getProductBySkuWhenProductDoesNotExist() { + when(productService.getProductBySku("1")).thenReturn(productNotFoundErrorMono); + testClient.get() + .uri("/product/1") + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void saveProduct() { + Product productToSave = new Product(null, "toy", new BigDecimal(200)); + when(productService.saveProduct(productToSave)).thenReturn(productMono()); + testClient.post() + .uri("/product") + .body(just(productToSave), Product.class) + .exchange() + .expectStatus().isCreated() + .expectBody(Product.class).isEqualTo(product1) + .consumeWith(result -> { + Product savedProduct = result.getResponseBody(); + assertNotNull(savedProduct); + assertNotNull(savedProduct.getSku()); + assertTrue(productPredicate1.test(savedProduct)); + }); + } + + @Test + void saveProductWithProductNameOrPriceAsNull() { + Product productToSave = new Product(); + testClient.post() + .uri("/product") + .body(just(productToSave), Product.class) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + void updateProduct() { + Product productToUpdate = new Product(null, "toy", new BigDecimal(300)); + String sku = "1"; + when(productService.updateProduct(sku, productToUpdate)).thenReturn(Mono.just(product3)); + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isAccepted() + .expectBody(Product.class).isEqualTo(product3) + .consumeWith(result -> { + Product updatedProduct = result.getResponseBody(); + assertNotNull(updatedProduct); + assertEquals(sku, updatedProduct.getSku()); + assertEquals(product3.getPrice(), updatedProduct.getPrice()); + assertTrue(productPredicate2.test(updatedProduct)); + }); + } + + @Test + void updateProductWhenProductNameOrPriceIsNull() { + Product productToUpdate = new Product(); + String sku = "5"; + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + void updateProductWhenProductDoesNotExistWithGivenSku() { + Product productToUpdate = new Product(null, "toy", new BigDecimal(300)); + String sku = "5"; + when(productService.updateProduct(sku, productToUpdate)).thenReturn(productNotFoundErrorMono); + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void removeProduct() { + String sku = "5"; + final String deletedText = "deleted"; + when(productService.removeProduct(sku)).thenReturn(Mono.just(deletedText)); + testClient.delete() + .uri("/product/" + sku) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(result -> assertEquals(deletedText, result.getResponseBody())); + } + + @Test + void removeProductWhenSkuDoesNotExist() { + String sku = "5"; + when(productService.removeProduct(sku)).thenReturn(Mono.error(ProductNotFoundException::new)); + testClient.delete() + .uri("/product/" + sku) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void getAllProducts() { + when(productService.getAllProducts()).thenReturn(productsFlux()); + + testClient.get().uri("/products") + .exchange() + .expectStatus().isOk() + .expectBodyList(Product.class) + .hasSize(2) + .contains(product1, product2); + + } + + private Mono<Product> productMono() { + return just(product1); + } + + private Flux<Product> productsFlux() { + return Flux.just(product1, product2); + } + + private Predicate<Product> productPredicate(String sku, String name, int price) { + return product -> product.getSku().equals(sku) + && product.getName().equals(name) + && product.getPrice().intValue() == price; + } + +} \ No newline at end of file diff --git a/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerIntegrationTests.java b/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerIntegrationTests.java new file mode 100644 index 0000000000000000000000000000000000000000..e5fafc7583cc9634626860200ffa6395460f3920 --- /dev/null +++ b/src/test/java/com/nisum/reactiveweb/controller/ProductHandlerIntegrationTests.java @@ -0,0 +1,171 @@ +package com.nisum.reactiveweb.controller; + +import com.nisum.reactiveweb.model.Product; +import com.nisum.reactiveweb.repository.ProductRepository; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static reactor.core.publisher.Mono.just; + +@SpringBootTest +@ActiveProfiles("test") +@TestMethodOrder(OrderAnnotation.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@AutoConfigureWebTestClient +class ProductHandlerIntegrationTests { + + @Autowired + private WebTestClient testClient; + + @Autowired + private ProductRepository repository; + + private String sku = ""; + + @BeforeAll + @AfterAll + void setup() { + repository.removeAllProducts().block(); + } + + @Test + @Order(3) + void getProductBySku() { + testClient.get() + .uri("/product/" + sku) + .exchange() + .expectStatus().isOk() + .expectBody(Product.class) + .consumeWith(result -> { + Product product = result.getResponseBody(); + assertNotNull(product); + }); + } + + @Test + void getProductBySkuWhenProductDoesNotExist() { + testClient.get() + .uri("/product/1") + .exchange() + .expectStatus().isNotFound(); + } + + @Test + @Order(1) + void saveProduct() { + Product productToSave = new Product(null, "toy", new BigDecimal(200)); + testClient.post() + .uri("/product") + .body(just(productToSave), Product.class) + .exchange() + .expectStatus().isCreated() + .expectBody(Product.class) + .consumeWith(result -> { + Product savedProduct = result.getResponseBody(); + assertNotNull(savedProduct); + assertNotNull(savedProduct.getSku()); + sku = savedProduct.getSku(); + assertEquals(productToSave.getName(), savedProduct.getName()); + assertEquals(productToSave.getPrice(), savedProduct.getPrice()); + }); + } + + @Test + void saveProductWithProductNameOrPriceAsNull() { + Product productToSave = new Product(); + testClient.post() + .uri("/product") + .body(just(productToSave), Product.class) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + @Order(2) + void updateProduct() { + String productName = "toy"; + BigDecimal productPrice = new BigDecimal(300); + Product productToUpdate = new Product(null, productName, productPrice); + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isAccepted() + .expectBody(Product.class) + .consumeWith(result -> { + Product updatedProduct = result.getResponseBody(); + assertNotNull(updatedProduct); + assertEquals(sku, updatedProduct.getSku()); + assertEquals(productName, updatedProduct.getName()); + assertEquals(productPrice, updatedProduct.getPrice()); + }); + } + + @Test + void updateProductWhenProductNameOrPriceIsNull() { + Product productToUpdate = new Product(); + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isBadRequest(); + } + + @Test + void updateProductWhenProductDoesNotExistWithGivenSku() { + Product productToUpdate = new Product(null, "toy", new BigDecimal(300)); + String sku = "5"; + testClient.put() + .uri("/product/" + sku) + .body(just(productToUpdate), Product.class) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + @Order(10) + void removeProduct() { + final String deletedText = "deleted"; + testClient.delete() + .uri("/product/" + sku) + .exchange() + .expectStatus().isOk() + .expectBody(String.class) + .consumeWith(result -> assertEquals(deletedText, result.getResponseBody())); + } + + @Test + void removeProductWhenSkuDoesNotExist() { + String sku = "5"; + testClient.delete() + .uri("/product/" + sku) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + @Order(3) + void getAllProducts() { + testClient.get().uri("/products") + .exchange() + .expectStatus().isOk() + .expectBodyList(Product.class) + .hasSize(1); + } + +} \ No newline at end of file diff --git a/src/test/java/com/nisum/reactiveweb/repository/ProductRepositoryTest.java b/src/test/java/com/nisum/reactiveweb/repository/ProductRepositoryTest.java deleted file mode 100644 index 1f87b93ddabdacab3bcc4c61f9cedd8345e17d0f..0000000000000000000000000000000000000000 --- a/src/test/java/com/nisum/reactiveweb/repository/ProductRepositoryTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.nisum.reactiveweb.repository; - -import org.junit.jupiter.api.Test; - -class ProductRepositoryTest { - - @Test - void getProductBySku() { - } - - @Test - void getAllProducts() { - } - - @Test - void saveProduct() { - } - - @Test - void updateProduct() { - } - - @Test - void removeProduct() { - } -} \ No newline at end of file