Files
spring-cloud-function/spring-cloud-function-samples/function-sample-cloudevent/src/test/java/io/spring/cloudevent/CloudeventDemoApplicationRESTTests.java
Oleg Zhurakousky ac54a83e09 Clean up and simplifications around CloudEvent processing
This commit effectively a merge of work with #607 and simplifies the following
- CloudEventAttributesProvider now provides CloudEventAttributes initialized with defaults to be set by the user
- In HTTP RequestProcessor the logic of sanitizing headers was improved to ensure that correct prefix is applied

Resolves #607
2020-11-16 13:40:53 +01:00

401 lines
18 KiB
Java

/*
* Copyright 2020-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.spring.cloudevent;
import static org.assertj.core.api.Assertions.assertThat;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeType;
import org.springframework.util.SocketUtils;
/**
*
* @author Oleg Zhurakousky
*
*/
public class CloudeventDemoApplicationRESTTests {
private TestRestTemplate testRestTemplate = new TestRestTemplate();
@BeforeEach
public void init() throws Exception {
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
}
/*
* This test demonstrates consumption of Cloud Event via HTTP POST - binary-mode message.
* According to specification - https://github.com/cloudevents/spec/blob/v1.0/spec.md
* - A "binary-mode message" is one where the event data is stored in the message body,
* and event attributes are stored as part of message meta-data.
*
* The above means that it fits perfectly with Spring Message model and as such there is
* absolutely nothing that needs to be done at the framework or user level to consume it.
* It just works!
*
* The example demonstrated via two types of functions
* - Function<Message<String>, String> asBinaryViaMessage;
* - Function<String, String> asJustBinary;
*/
@Test
public void testAsBinaryMessageViaHTTP() throws Exception {
SpringApplication.run(CloudeventDemoApplication.class);
HttpHeaders headers = this.buildHeaders(MediaType.APPLICATION_JSON);
// will work with either content type
// HttpHeaders headers = this.buildHeaders(MediaType.valueOf("application/cloudevents+json;charset=utf-8"));
String payload = "{\"releaseDate\":\"2004-03-24\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asStringMessage"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo(payload);
re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asString"));
response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo(payload);
}
/*
* The same as the previous two tests with the exception that cloud event data de-serialized into POJO.
* Again, given that abstractions for transparent type conversion already part of the Spring ecosystem nothing
* needed to be done at the framework or user level to consume it.
* It just works!
*/
@Test
public void testAsBinaryPOJOMessageViaHTTP() throws Exception {
SpringApplication.run(CloudeventDemoApplication.class);
HttpHeaders headers = this.buildHeaders(MediaType.APPLICATION_JSON);
String payload = "{\"releaseDate\":\"24-03-2004\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asPOJOMessage"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("releaseDate:24-03-2004; releaseName:Spring Framework; version:1.0");
re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asPOJO"));
response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("releaseDate:24-03-2004; releaseName:Spring Framework; version:1.0");
}
/*
* This test demonstrates parsing of cloud event out of provided 'datacontenttype'
* using custom message converter which supports imaginary "contentType=foo/bar".
*
*/
@Test
public void testAsBinaryPOJOMessageViaHTTPCustomDataType() throws Exception {
SpringApplication.run(new Class[] {CloudeventDemoApplication.class, FooBarConverterConfiguration.class}, new String[] {});
HttpHeaders headers = this.buildHeaders(MediaType.valueOf("application/cloudevents+json;charset=utf-8"));
headers.set("datacontenttype", "foo/bar");
String payload = "24-03-2004:Spring Framework:1.0";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asPOJOMessage"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("releaseDate:24-03-2004; releaseName:Spring Framework; version:1.0");
}
/*
* This test demonstrates sending structured
*/
@Test
public void testAsStracturalFormatToPOJO() throws Exception {
SpringApplication.run(CloudeventDemoApplication.class);
String payload = "{\n" +
" \"specversion\" : \"1.0\",\n" +
" \"type\" : \"org.springframework\",\n" +
" \"source\" : \"https://spring.io/\",\n" +
" \"id\" : \"A234-1234-1234\",\n" +
" \"datacontenttype\" : \"application/json\",\n" +
" \"data\" : {\n" +
" \"version\" : \"1.0\",\n" +
" \"releaseName\" : \"Spring Framework\",\n" +
" \"releaseDate\" : \"24-03-2004\"\n" +
" }\n" +
"}";
System.out.println(payload);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/cloudevents+json;charset=utf-8"));
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asPOJOMessage"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("releaseDate:24-03-2004; releaseName:Spring Framework; version:1.0");
re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asPOJO"));
response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("releaseDate:24-03-2004; releaseName:Spring Framework; version:1.0");
}
@Test
public void testAsStracturalFormatToString() throws Exception {
SpringApplication.run(CloudeventDemoApplication.class);
String payload = "{\n" +
" \"ce-specversion\" : \"1.0\",\n" +
" \"ce-type\" : \"org.springframework\",\n" +
" \"ce-source\" : \"https://spring.io/\",\n" +
" \"ce-id\" : \"A234-1234-1234\",\n" +
" \"ce-datacontenttype\" : \"application/json\",\n" +
" \"ce-data\" : {\n" +
" \"version\" : \"1.0\",\n" +
" \"releaseName\" : \"Spring Framework\",\n" +
" \"releaseDate\" : \"24-03-2004\"\n" +
" }\n" +
"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/cloudevents+json;charset=utf-8"));
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asStringMessage"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("{\"version\":\"1.0\",\"releaseName\":\"Spring Framework\",\"releaseDate\":\"24-03-2004\"}");
re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/asString"));
response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("{\"version\":\"1.0\",\"releaseName\":\"Spring Framework\",\"releaseDate\":\"24-03-2004\"}");
}
@Test
public void testAsBinaryMapToMap() throws Exception {
SpringApplication.run(new Class[] {CloudeventDemoApplication.class}, new String[] {});
HttpHeaders headers = this.buildHeaders(MediaType.APPLICATION_JSON);
String payload = "{\"releaseDate\":\"24-03-2004\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/consumeAndProduceCloudEventAsMapToMap"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("{\"releaseDate\":\"01-10-2050\",\"releaseName\":\"Spring Framework\",\"version\":\"10.0\"}");
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.SOURCE))
.isEqualTo(Collections.singletonList("http://spring.io/application-application"));
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.TYPE))
.isEqualTo(Collections.singletonList(LinkedHashMap.class.getName()));
}
@Test
public void testAsBinaryPojoToPojo() throws Exception {
SpringApplication.run(new Class[] {CloudeventDemoApplication.class}, new String[] {});
HttpHeaders headers = this.buildHeaders(MediaType.APPLICATION_JSON);
String payload = "{\"releaseDate\":\"01-10-2006\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/consumeAndProduceCloudEventAsPojoToPojo"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("{\"releaseDate\":\"01-10-2006\",\"releaseName\":\"Spring Framework\",\"version\":\"2.0\"}");
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.SOURCE))
.isEqualTo(Collections.singletonList("http://spring.io/application-application"));
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.TYPE))
.isEqualTo(Collections.singletonList(SpringReleaseEvent.class.getName()));
}
/*
* Typically this would never happen since spec mandates that HTTP uses 'ce-` prefix.
* So this is to primarily validate that we can recognize it process it and still produce correct headers
*/
@Test
public void testAsBinaryPojoToPojoWrongHeaders() throws Exception {
SpringApplication.run(new Class[] {CloudeventDemoApplication.class}, new String[] {});
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.ID, UUID.randomUUID().toString());
headers.set(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.SOURCE, "https://spring.io/");
headers.set(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.SPECVERSION, "1.0");
headers.set(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.TYPE, "org.springframework");
String payload = "{\"releaseDate\":\"01-10-2006\", \"releaseName\":\"Spring Framework\", \"version\":\"1.0\"}";
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/consumeAndProduceCloudEventAsPojoToPojo"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
assertThat(response.getBody()).isEqualTo("{\"releaseDate\":\"01-10-2006\",\"releaseName\":\"Spring Framework\",\"version\":\"2.0\"}");
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.SOURCE))
.isEqualTo(Collections.singletonList("http://spring.io/application-application"));
assertThat(response.getHeaders().get(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.TYPE))
.isEqualTo(Collections.singletonList(SpringReleaseEvent.class.getName()));
assertThat(response.getHeaders().get(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.TYPE)).isNull();
assertThat(response.getHeaders().get(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.SOURCE)).isNull();
assertThat(response.getHeaders().get(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.ID)).isNull();
assertThat(response.getHeaders().get(CloudEventMessageUtils.ATTR_PREFIX + CloudEventMessageUtils.SPECVERSION)).isNull();
}
@Test
public void testAsStructuralPojoToPojo() throws Exception {
ApplicationContext context = SpringApplication.run(CloudeventDemoApplication.class);
JsonMapper mapper = context.getBean(JsonMapper.class);
String payload = "{\n" +
" \"specversion\" : \"1.0\",\n" +
" \"type\" : \"org.springframework\",\n" +
" \"source\" : \"https://spring.io/\",\n" +
" \"id\" : \"A234-1234-1234\",\n" +
// " \"ce-datacontenttype\" : \"application/json\",\n" +
" \"data\" : {\n" +
" \"version\" : \"1.0\",\n" +
" \"releaseName\" : \"Spring Framework\",\n" +
" \"releaseDate\" : \"24-03-2004\"\n" +
" }\n" +
"}";
System.out.println(payload);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.valueOf("application/cloudevents+json;charset=utf-8"));
RequestEntity<String> re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/consumeAndProduceCloudEventAsPojoToPojo"));
ResponseEntity<String> response = testRestTemplate.exchange(re, String.class);
SpringReleaseEvent springReleaseEvent = mapper.fromJson(response.getBody(), SpringReleaseEvent.class);
assertThat(springReleaseEvent.getReleaseName()).isEqualTo("Spring Framework");
assertThat(springReleaseEvent.getVersion()).isEqualTo("2.0");
re = new RequestEntity<>(payload, headers, HttpMethod.POST, this.constructURI("/consumeAndProduceCloudEventAsMapToMap"));
response = testRestTemplate.exchange(re, String.class);
springReleaseEvent = mapper.fromJson(response.getBody(), SpringReleaseEvent.class);
assertThat(springReleaseEvent.getReleaseName()).isEqualTo("Spring Framework");
assertThat(springReleaseEvent.getVersion()).isEqualTo("10.0");
// assertThat(response.getHeaders().get(CloudEventMessageUtils.CANONICAL_SOURCE))
// .isEqualTo(Collections.singletonList("http://spring.io/application-application"));
// assertThat(response.getHeaders().get(CloudEventMessageUtils.CANONICAL_TYPE))
// .isEqualTo(Collections.singletonList(SpringReleaseEvent.class.getName()));
}
private URI constructURI(String path) throws Exception {
return new URI("http://localhost:" + System.getProperty("server.port") + path);
}
private HttpHeaders buildHeaders(MediaType contentType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(contentType);
headers.set(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.ID, UUID.randomUUID().toString());
headers.set(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.SOURCE, "https://spring.io/");
headers.set(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.SPECVERSION, "1.0");
headers.set(CloudEventMessageUtils.HTTP_ATTR_PREFIX + CloudEventMessageUtils.TYPE, "org.springframework");
return headers;
}
@Configuration
public static class FooBarConverterConfiguration {
@Bean
public MessageConverter foobar(JsonMapper jsonMapper) {
return new FooBarToCloudEventMessageConverter(jsonMapper);
}
}
public static class FooBarToCloudEventMessageConverter extends AbstractMessageConverter {
public FooBarToCloudEventMessageConverter(JsonMapper jsonMapper) {
super(new MimeType("foo", "bar"));
}
@Override
protected boolean supports(Class<?> clazz) {
throw new UnsupportedOperationException();
}
@Override
protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) {
if (!supportsMimeType(headers)) {
return false;
}
return true;
}
@Override
protected boolean canConvertFrom(Message<?> message, @Nullable Class<?> targetClass) {
if (targetClass == null || !supportsMimeType(message.getHeaders())) {
return false;
}
else if (message.getHeaders().containsKey("datacontenttype") && message.getHeaders().get("datacontenttype").equals("foo/bar")) {
return true;
}
return false;
}
@Override
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
if (message.getHeaders().containsKey("datacontenttype")
&& message.getHeaders().get("datacontenttype").equals("foo/bar")
&& SpringReleaseEvent.class == targetClass) {
SpringReleaseEvent event = new SpringReleaseEvent();
String[] data = ((String) message.getPayload()).split(":");
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy");
try {
event.setReleaseDate(df.parse(data[0].trim()));
}
catch (Exception e) {
throw new IllegalArgumentException("Failed to convert date", e);
}
event.setReleaseName(data[1]);
event.setVersion(data[2]);
return event;
}
else {
return super.convertFromInternal(message, targetClass, conversionHint);
}
}
@Override
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers,
@Nullable Object conversionHint) {
return null;
}
}
}