From 3af60557bd3463ca56a1c7322569dc635dcf10d7 Mon Sep 17 00:00:00 2001 From: Gerard Klijs Date: Mon, 23 Jan 2023 23:24:35 +0100 Subject: [PATCH] BAEL-5856 Using and testing Axon applications via REST (#13134) Co-authored-by: Ashley Frieze --- axon/README.md | 3 +- axon/pom.xml | 13 +- .../gui/OrderRestEndpointIntegrationTest.java | 187 ++++++++++++++++++ 3 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 axon/src/test/java/com/baeldung/axon/gui/OrderRestEndpointIntegrationTest.java diff --git a/axon/README.md b/axon/README.md index 2dad554110..9aeef05dd6 100644 --- a/axon/README.md +++ b/axon/README.md @@ -20,4 +20,5 @@ Two scripts are included to easily start middleware using Docker matching the pr - [Multi-Entity Aggregates in Axon](https://www.baeldung.com/java-axon-multi-entity-aggregates) - [Snapshotting Aggregates in Axon](https://www.baeldung.com/axon-snapshotting-aggregates) - [Dispatching Queries in Axon Framework](https://www.baeldung.com/axon-query-dispatching) -- [Persisting the Query Model](https://www.baeldung.com/axon-persisting-query-model) +- [Persisting the Query Model](https://www.baeldung.com/persisting-the-query-model) +- [Using and testing Axon applications via REST](https://www.baeldung.com/using-and-testing-axon-applications-via-rest) diff --git a/axon/pom.xml b/axon/pom.xml index cfdd24ff3c..42e32758ea 100644 --- a/axon/pom.xml +++ b/axon/pom.xml @@ -81,13 +81,22 @@ org.awaitility awaitility - 4.2.0 + test + + + org.springframework + spring-webflux + test + + + io.projectreactor.netty + reactor-netty-http test - 4.6.2 + 4.6.3 3.4.8 diff --git a/axon/src/test/java/com/baeldung/axon/gui/OrderRestEndpointIntegrationTest.java b/axon/src/test/java/com/baeldung/axon/gui/OrderRestEndpointIntegrationTest.java new file mode 100644 index 0000000000..a30e42766b --- /dev/null +++ b/axon/src/test/java/com/baeldung/axon/gui/OrderRestEndpointIntegrationTest.java @@ -0,0 +1,187 @@ +package com.baeldung.axon.gui; + +import com.baeldung.axon.OrderApplication; +import com.baeldung.axon.querymodel.OrderResponse; +import com.baeldung.axon.querymodel.OrderStatusResponse; + +import org.junit.jupiter.api.*; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(classes = OrderApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class OrderRestEndpointIntegrationTest { + + @LocalServerPort + private int port; + + @Test + @DirtiesContext + void givenCreateOrderCalled_whenCallingAllOrders_thenOneCreatedOrderIsReturned() { + WebClient client = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + createRandomNewOrder(client); + StepVerifier.create(retrieveListResponse(client.get() + .uri("http://localhost:" + port + "/all-orders"))) + .expectNextMatches(list -> 1 == list.size() && list.get(0) + .getOrderStatus() == OrderStatusResponse.CREATED) + .verifyComplete(); + } + + @Test + @DirtiesContext + void givenCreateOrderCalledThreeTimesAnd_whenCallingAllOrdersStreaming_thenTwoCreatedOrdersAreReturned() { + WebClient client = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + for (int i = 0; i < 3; i++) { + createRandomNewOrder(client); + } + StepVerifier.create(retrieveStreamingResponse(client.get() + .uri("http://localhost:" + port + "/all-orders-streaming"))) + .expectNextMatches(o -> o.getOrderStatus() == OrderStatusResponse.CREATED) + .expectNextMatches(o -> o.getOrderStatus() == OrderStatusResponse.CREATED) + .expectNextMatches(o -> o.getOrderStatus() == OrderStatusResponse.CREATED) + .verifyComplete(); + } + + @Test + @DirtiesContext + void givenRuleExistThatNeedConfirmationBeforeShipping_whenCallingShipUnconfirmed_thenErrorReturned() { + WebClient client = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + StepVerifier.create(retrieveResponse(client.post() + .uri("http://localhost:" + port + "/ship-unconfirmed-order"))) + .verifyError(WebClientResponseException.class); + } + + @Test + @DirtiesContext + void givenShipOrderCalled_whenCallingAllShippedChairs_then234PlusOneIsReturned() { + WebClient client = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + verifyVoidPost(client, "http://localhost:" + port + "/ship-order"); + StepVerifier.create(retrieveIntegerResponse(client.get() + .uri("http://localhost:" + port + "/total-shipped/Deluxe Chair"))) + .assertNext(r -> assertEquals(235, r)) + .verifyComplete(); + } + + @Test + @DirtiesContext + void givenOrdersAreUpdated_whenCallingOrderUpdates_thenUpdatesReturned() { + WebClient updaterClient = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + WebClient receiverClient = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + String orderId = UUID.randomUUID() + .toString(); + String productId = UUID.randomUUID() + .toString(); + StepVerifier.create(retrieveResponse(updaterClient.post() + .uri("http://localhost:" + port + "/order/" + orderId))) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.schedule(() -> addIncrementDecrementConfirmAndShipProduct(orderId, productId), 1L, TimeUnit.SECONDS); + try { + StepVerifier.create(retrieveStreamingResponse(receiverClient.get() + .uri("http://localhost:" + port + "/order-updates/" + orderId))) + .assertNext(p -> assertTrue(p.getProducts() + .isEmpty())) + .assertNext(p -> assertEquals(1, p.getProducts() + .get(productId))) + .assertNext(p -> assertEquals(2, p.getProducts() + .get(productId))) + .assertNext(p -> assertEquals(1, p.getProducts() + .get(productId))) + .assertNext(p -> assertEquals(OrderStatusResponse.CONFIRMED, p.getOrderStatus())) + .assertNext(p -> assertEquals(OrderStatusResponse.SHIPPED, p.getOrderStatus())) + .thenCancel() + .verify(); + } finally { + executor.shutdown(); + } + } + + private void addIncrementDecrementConfirmAndShipProduct(String orderId, String productId) { + WebClient client = WebClient.builder() + .clientConnector(httpConnector()) + .build(); + String base = "http://localhost:" + port + "/order/" + orderId; + verifyVoidPost(client, base + "/product/" + productId); + verifyVoidPost(client, base + "/product/" + productId + "/increment"); + verifyVoidPost(client, base + "/product/" + productId + "/decrement"); + verifyVoidPost(client, base + "/confirm"); + verifyVoidPost(client, base + "/ship"); + } + + private void createRandomNewOrder(WebClient client){ + StepVerifier.create(retrieveResponse(client.post() + .uri("http://localhost:" + port + "/order"))) + .assertNext(Assertions::assertNotNull) + .verifyComplete(); + } + + private void verifyVoidPost(WebClient client, String uri) { + StepVerifier.create(retrieveResponse(client.post() + .uri(uri))) + .verifyComplete(); + } + + private static ReactorClientHttpConnector httpConnector() { + HttpClient httpClient = HttpClient.create() + .wiretap(true); + return new ReactorClientHttpConnector(httpClient); + } + + private Mono retrieveResponse(WebClient.RequestBodySpec spec) { + return spec.retrieve() + .bodyToMono(String.class); + } + + private Mono retrieveListResponse(WebClient.RequestHeadersSpec spec) { + return spec.accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(ResponseList.class); + } + + private Mono retrieveIntegerResponse(WebClient.RequestHeadersSpec spec) { + return spec.retrieve() + .bodyToMono(Integer.class); + } + + private Flux retrieveStreamingResponse(WebClient.RequestHeadersSpec spec) { + return spec.retrieve() + .bodyToFlux(OrderResponse.class); + } + + private static class ResponseList extends ArrayList { + + private ResponseList() { + super(); + } + } +}