From 9231e6d230a54037d0f03c246bb2dae2078e018c Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Wed, 17 Nov 2021 15:25:59 +0000 Subject: [PATCH] Add example of Spring WebClient usage (#418) - Add example of WebClient usage - Add additional test case with WebClient usage Signed-off-by: Dave Syer --- docs/spring.md | 12 + .../examples/spring/DemoApplication.java | 14 +- .../examples/spring/DemoApplicationTests.java | 226 ++++++++++-------- .../examples/spring/WebClientTests.java | 58 +++++ 4 files changed, 207 insertions(+), 103 deletions(-) create mode 100644 examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/WebClientTests.java diff --git a/docs/spring.md b/docs/spring.md index f5bd87dd4..d3f9fdf6e 100644 --- a/docs/spring.md +++ b/docs/spring.md @@ -115,6 +115,18 @@ public Mono event(@RequestBody Mono body) { } ``` +The `CodecCustomizer` also works on the client side, so you can use it anywhere that you use a `WebClient` (including in an MVC application). Here's a simple example of a Cloud Event HTTP client: + +```java +WebClient client = ...; // Either WebClient.create() or @Autowired a WebClient.Builder +CloudEvent event = ...; // Create a CloudEvent +Mono response = client.post() + .uri("http://localhost:8080/events") + .bodyValue(event) + .retrieve() + .bodyToMono(CloudEvent.class); +``` + ### Messaging Spring Messaging is applicable in a wide range of use cases including WebSockets, JMS, Apache Kafka, RabbitMQ and others. It is also a core part of the Spring Cloud Function and Spring Cloud Stream libraries, so those are natural tools to use to build applications that use Cloud Events. The core abstraction in Spring is the `Message` which carries headers and a payload, just like a `CloudEvent`. Since the mapping is quite direct it makes sense to have a set of converters for Spring applications, so you can consume and produce `CloudEvents`, by treating them as `Messages`. This project provides a converter that you can register in a Spring Messaging application: diff --git a/examples/spring-function/src/main/java/io/cloudevents/examples/spring/DemoApplication.java b/examples/spring-function/src/main/java/io/cloudevents/examples/spring/DemoApplication.java index a646d177b..2c999774c 100644 --- a/examples/spring-function/src/main/java/io/cloudevents/examples/spring/DemoApplication.java +++ b/examples/spring-function/src/main/java/io/cloudevents/examples/spring/DemoApplication.java @@ -4,22 +4,20 @@ import java.util.UUID; import java.util.function.Function; -import io.cloudevents.CloudEvent; -import io.cloudevents.core.builder.CloudEventBuilder; -import io.cloudevents.spring.messaging.CloudEventMessageConverter; -import io.cloudevents.spring.webflux.CloudEventHttpMessageReader; -import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.codec.CodecConfigurer; -import org.springframework.web.bind.annotation.RestController; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import io.cloudevents.spring.messaging.CloudEventMessageConverter; +import io.cloudevents.spring.webflux.CloudEventHttpMessageReader; +import io.cloudevents.spring.webflux.CloudEventHttpMessageWriter; @SpringBootApplication -@RestController public class DemoApplication { public static void main(String[] args) throws Exception { diff --git a/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/DemoApplicationTests.java b/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/DemoApplicationTests.java index 94c465563..2e691671c 100644 --- a/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/DemoApplicationTests.java +++ b/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/DemoApplicationTests.java @@ -3,110 +3,146 @@ import java.net.URI; import org.junit.jupiter.api.Test; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import org.springframework.http.RequestEntity; -import org.springframework.http.ResponseEntity; +import org.springframework.test.web.reactive.server.WebTestClient; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class DemoApplicationTests { - @Autowired - private TestRestTemplate rest; - - @LocalServerPort - private int port; - - @Test - void echoWithCorrectHeaders() { - - ResponseEntity response = rest - .exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/foos")) // - .header("ce-id", "12345") // - .header("ce-specversion", "1.0") // - .header("ce-type", "io.spring.event") // - .header("ce-source", "https://spring.io/events") // - .contentType(MediaType.APPLICATION_JSON) // - .body("{\"value\":\"Dave\"}"), String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}"); - - HttpHeaders headers = response.getHeaders(); - - assertThat(headers).containsKey("ce-id"); - assertThat(headers).containsKey("ce-source"); - assertThat(headers).containsKey("ce-type"); - - assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345"); - assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo"); - assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos"); - - } - - @Test - void structuredRequestResponseEvents() { - - ResponseEntity response = rest - .exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) // - .contentType(new MediaType("application", "cloudevents+json")) // - .body("{" // - + "\"id\":\"12345\"," // - + "\"specversion\":\"1.0\"," // - + "\"type\":\"io.spring.event\"," // - + "\"source\":\"https://spring.io/events\"," // - + "\"data\":{\"value\":\"Dave\"}}"), - String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}"); - - HttpHeaders headers = response.getHeaders(); - - assertThat(headers).containsKey("ce-id"); - assertThat(headers).containsKey("ce-source"); - assertThat(headers).containsKey("ce-type"); - - assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345"); - assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo"); - assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos"); - - } - - @Test - void requestResponseEvents() { - - ResponseEntity response = rest - .exchange(RequestEntity.post(URI.create("http://localhost:" + port + "/event")) // - .header("ce-id", "12345") // - .header("ce-specversion", "1.0") // - .header("ce-type", "io.spring.event") // - .header("ce-source", "https://spring.io/events") // - .contentType(MediaType.APPLICATION_JSON) // - .body("{\"value\":\"Dave\"}"), String.class); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo("{\"value\":\"Dave\"}"); - - HttpHeaders headers = response.getHeaders(); - - assertThat(headers).containsKey("ce-id"); - assertThat(headers).containsKey("ce-source"); - assertThat(headers).containsKey("ce-type"); - - assertThat(headers.getFirst("ce-id")).isNotEqualTo("12345"); - assertThat(headers.getFirst("ce-type")).isEqualTo("io.spring.event.Foo"); - assertThat(headers.getFirst("ce-source")).isEqualTo("https://spring.io/foos"); - - } + @Autowired + private WebTestClient rest; + + @Test + void echoWithCorrectHeaders() { + + rest.post().uri("/foos").header("ce-id", "12345") // + .header("ce-specversion", "1.0") // + .header("ce-type", "io.spring.event") // + .header("ce-source", "https://spring.io/events") // + .contentType(MediaType.APPLICATION_JSON) // + .bodyValue("{\"value\":\"Dave\"}") // + .exchange() // + .expectStatus().isOk() // + .expectHeader().exists("ce-id") // + .expectHeader().exists("ce-source") // + .expectHeader().exists("ce-type") // + .expectHeader().value("ce-id", value -> { + if (value.equals("12345")) + throw new IllegalStateException(); + }) // + .expectHeader().valueEquals("ce-type", "io.spring.event.Foo") // + .expectHeader().valueEquals("ce-source", "https://spring.io/foos") // + .expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}"); + + } + + @Test + void structuredRequestResponseEvents() { + + rest.post().uri("/event") // + .contentType(new MediaType("application", "cloudevents+json")) // + .bodyValue("{" // + + "\"id\":\"12345\"," // + + "\"specversion\":\"1.0\"," // + + "\"type\":\"io.spring.event\"," // + + "\"source\":\"https://spring.io/events\"," // + + "\"data\":{\"value\":\"Dave\"}}") // + .exchange() // + .expectStatus().isOk() // + .expectHeader().exists("ce-id") // + .expectHeader().exists("ce-source") // + .expectHeader().exists("ce-type") // + .expectHeader().value("ce-id", value -> { + if (value.equals("12345")) + throw new IllegalStateException(); + }) // + .expectHeader().valueEquals("ce-type", "io.spring.event.Foo") // + .expectHeader().valueEquals("ce-source", "https://spring.io/foos") // + .expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}"); + + } + + @Test + void structuredRequestResponseCloudEventToString() { + + rest.post().uri("/event") // + .bodyValue(CloudEventBuilder.v1() // + .withId("12345") // + .withType("io.spring.event") // + .withSource(URI.create("https://spring.io/events")).withData("{\"value\":\"Dave\"}".getBytes()) // + .build()) // + .exchange() // + .expectStatus().isOk() // + .expectHeader().exists("ce-id") // + .expectHeader().exists("ce-source") // + .expectHeader().exists("ce-type") // + .expectHeader().value("ce-id", value -> { + if (value.equals("12345")) + throw new IllegalStateException(); + }) // + .expectHeader().valueEquals("ce-type", "io.spring.event.Foo") // + .expectHeader().valueEquals("ce-source", "https://spring.io/foos") // + .expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}"); + + } + + @Test + void structuredRequestResponseCloudEventToCloudEvent() { + + rest.post().uri("/event") // + .accept(new MediaType("application", "cloudevents+json")) // + .bodyValue(CloudEventBuilder.v1() // + .withId("12345") // + .withType("io.spring.event") // + .withSource(URI.create("https://spring.io/events")) // + .withData("{\"value\":\"Dave\"}".getBytes()) // + .build()) // + .exchange() // + .expectStatus().isOk() // + .expectHeader().exists("ce-id") // + .expectHeader().exists("ce-source") // + .expectHeader().exists("ce-type") // + .expectHeader().value("ce-id", value -> { + if (value.equals("12345")) + throw new IllegalStateException(); + }) // + .expectHeader().valueEquals("ce-type", "io.spring.event.Foo") // + .expectHeader().valueEquals("ce-source", "https://spring.io/foos") // + .expectBody(CloudEvent.class) // + .value(event -> assertThat(new String(event.getData().toBytes())) // + .isEqualTo("{\"value\":\"Dave\"}")); + + } + + @Test + void requestResponseEvents() { + + rest.post().uri("/event").header("ce-id", "12345") // + .header("ce-specversion", "1.0") // + .header("ce-type", "io.spring.event") // + .header("ce-source", "https://spring.io/events") // + .contentType(MediaType.APPLICATION_JSON) // + .bodyValue("{\"value\":\"Dave\"}") // + .exchange() // + .expectStatus().isOk() // + .expectHeader().exists("ce-id") // + .expectHeader().exists("ce-source") // + .expectHeader().exists("ce-type") // + .expectHeader().value("ce-id", value -> { + if (value.equals("12345")) + throw new IllegalStateException(); + }) // + .expectHeader().valueEquals("ce-type", "io.spring.event.Foo") // + .expectHeader().valueEquals("ce-source", "https://spring.io/foos") // + .expectBody(String.class).isEqualTo("{\"value\":\"Dave\"}"); + + } } diff --git a/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/WebClientTests.java b/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/WebClientTests.java new file mode 100644 index 000000000..590a4f975 --- /dev/null +++ b/examples/spring-reactive/src/test/java/io/cloudevents/examples/spring/WebClientTests.java @@ -0,0 +1,58 @@ +package io.cloudevents.examples.spring; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.web.reactive.function.client.WebClient; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import reactor.core.publisher.Mono; + +/** + * Test case to show example usage of WebClient and CloudEvent. The actual + * content of the request and response are asserted separately in + * {@link DemoApplicationTests}. + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class WebClientTests { + + @Autowired + private WebClient.Builder rest; + + @LocalServerPort + private int port; + + private CloudEvent event; + + @BeforeEach + void setUp() { + event = CloudEventBuilder.v1() // + .withId("12345") // + .withSource(URI.create("https://spring.io/events")) // + .withType("io.spring.event") // + .withData("{\"value\":\"Dave\"}".getBytes()) // + .build(); + } + + @Test + void echoWithCorrectHeaders() { + + Mono result = rest.build() // + .post() // + .uri("http://localhost:" + port + "/event") // + .bodyValue(event) // + .exchangeToMono(response -> response.bodyToMono(CloudEvent.class)); + + assertThat(result.block().getData()).isEqualTo(event.getData()); + + } + +}