Split test-binder from core spring-cloud-stream

* Remove all SI test-binder based components from core spring-cloud-stream-module
* Create a new module - spring-cloud-stream-test-binder that contains the test-binder
  and all it's related components
* Migrate tests from core module that use the test-binder into a separate module
  called spring-cloud-stream-integration-tests
* Remove the test-jar dependency using the classifier approach
* Update Spring Cloud Stream BOM with the new test-binder dependency
* Update Schema-Registry tests that use the old approach (using the test-jar with the classifier)
  with the new test-binder dependency

Resolves https://github.com/spring-cloud/spring-cloud-stream/issues/2565
This commit is contained in:
Soby Chacko
2022-11-22 18:28:49 -05:00
committed by Oleg Zhurakousky
parent 58ef5b0479
commit bc094e0ec4
44 changed files with 1382 additions and 1317 deletions

View File

@@ -0,0 +1,153 @@
/*
* Copyright 2017-2019 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 org.springframework.cloud.stream.binder;
import java.util.Iterator;
import java.util.Set;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.provisioning.ProvisioningProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.handler.BridgeHandler;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.SubscribableChannel;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Gary Russell
* @author Oleg Zhurakousky
* @since 1.2.2
*/
@Disabled("This test has issues that needs to be looked into")
public class BasicAbstractMessageChannelBinderTests {
private static ApplicationContext context;
@BeforeAll
public static void prepare() {
context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration())
.web(WebApplicationType.NONE).run();
}
@Test
@SuppressWarnings("unchecked")
public void testEndpointLifecycle() throws Exception {
// @checkstyle:off
AbstractMessageChannelBinder<ConsumerProperties, ProducerProperties, ProvisioningProvider<ConsumerProperties, ProducerProperties>> binder = context
.getBean(AbstractMessageChannelBinder.class);
// @checkstyle:on
ConsumerProperties consumerProperties = new ConsumerProperties();
consumerProperties.setMaxAttempts(1); // to force error infrastructure creation
Binding<MessageChannel> consumerBinding = binder.bindConsumer("foo", "fooGroup",
new DirectChannel(), consumerProperties);
DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor(
consumerBinding);
MessageProducer messageProducer = (MessageProducer) consumerBindingAccessor
.getPropertyValue("lifecycle");
assertThat(((Lifecycle) messageProducer).isRunning()).isTrue();
assertThat(messageProducer.getOutputChannel()).isNotNull();
SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor
.getPropertyValue("lifecycle.errorChannel");
assertThat(errorChannel).isNotNull();
Set<MessageHandler> handlers = TestUtils.getPropertyValue(errorChannel,
"dispatcher.handlers", Set.class);
assertThat(handlers.size()).isEqualTo(2);
Iterator<MessageHandler> iterator = handlers.iterator();
assertThat(iterator.next()).isInstanceOf(BridgeHandler.class);
assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class);
assertThat(this.context.containsBean("foo.fooGroup.errors")).isTrue();
assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isTrue();
assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isTrue();
assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isTrue();
consumerBinding.unbind();
assertThat(this.context.containsBean("foo.fooGroup.errors")).isFalse();
assertThat(this.context.containsBean("foo.fooGroup.errors.recoverer")).isFalse();
assertThat(this.context.containsBean("foo.fooGroup.errors.handler")).isFalse();
assertThat(this.context.containsBean("foo.fooGroup.errors.bridge")).isFalse();
assertThat(((Lifecycle) messageProducer).isRunning()).isFalse();
ProducerProperties producerProps = new ProducerProperties();
producerProps.setErrorChannelEnabled(true);
Binding<MessageChannel> producerBinding = binder.bindProducer("bar",
new DirectChannel(), producerProps);
assertThat(this.context.containsBean("bar.errors")).isTrue();
assertThat(this.context.containsBean("bar.errors.bridge")).isTrue();
producerBinding.unbind();
assertThat(this.context.containsBean("bar.errors")).isFalse();
assertThat(this.context.containsBean("bar.errors.bridge")).isFalse();
}
@Test
@SuppressWarnings("unchecked")
public void testEndpointBinderHasRecoverer() throws Exception {
// @checkstyle:off
ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration())
.web(WebApplicationType.NONE).run();
AbstractMessageChannelBinder<ConsumerProperties, ProducerProperties, ProvisioningProvider<ConsumerProperties, ProducerProperties>> binder = context
.getBean(AbstractMessageChannelBinder.class);
// @checkstyle:on
Binding<MessageChannel> consumerBinding = binder.bindConsumer("foo", "fooGroup",
new DirectChannel(), new ConsumerProperties());
DirectFieldAccessor consumerBindingAccessor = new DirectFieldAccessor(
consumerBinding);
SubscribableChannel errorChannel = (SubscribableChannel) consumerBindingAccessor
.getPropertyValue("lifecycle.errorChannel");
assertThat(errorChannel).isNull();
errorChannel = (SubscribableChannel) consumerBindingAccessor
.getPropertyValue("lifecycle.recoveryCallback.channel");
assertThat(errorChannel).isNotNull();
Set<MessageHandler> handlers = TestUtils.getPropertyValue(errorChannel,
"dispatcher.handlers", Set.class);
assertThat(handlers.size()).isEqualTo(2);
Iterator<MessageHandler> iterator = handlers.iterator();
assertThat(iterator.next()).isInstanceOf(BridgeHandler.class);
assertThat(iterator.next()).isInstanceOf(LastSubscriberMessageHandler.class);
assertThat(context.containsBean("foo.fooGroup.errors")).isTrue();
assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isTrue();
assertThat(context.containsBean("foo.fooGroup.errors.handler")).isTrue();
assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isTrue();
consumerBinding.unbind();
assertThat(context.containsBean("foo.fooGroup.errors")).isFalse();
assertThat(context.containsBean("foo.fooGroup.errors.recoverer")).isFalse();
assertThat(context.containsBean("foo.fooGroup.errors.handler")).isFalse();
assertThat(context.containsBean("foo.fooGroup.errors.bridge")).isFalse();
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2017-2022 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 org.springframework.cloud.stream.binder;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Anshul Mehra
*/
public class BinderErrorChannelTests {
private static final LastSubscriberMessageHandler FINAL_HANDLER = message -> {
};
private static final MessageHandler FIRST_HANDLER = message -> {
};
private static final MessageHandler SECOND_HANDLER = message -> {
};
@Test
void testExceptionIsThrownWhenNoSubscribers() {
BinderErrorChannel channel = new BinderErrorChannel();
Assertions.assertThrows(MessageDeliveryException.class, () -> {
channel.send(new GenericMessage<String>("hello"));
});
}
@Test
void testSubscribeUnsubscribe() {
BinderErrorChannel channel = new BinderErrorChannel();
DirectFieldAccessor fieldAccessor = new DirectFieldAccessor(channel);
AbstractDispatcher dispatcher = (AbstractDispatcher) fieldAccessor
.getPropertyValue("dispatcher");
assertThat(dispatcher).isNotNull();
assertThat(channel.subscribers()).isEqualTo(0);
assertThat(dispatcher.getHandlerCount()).isEqualTo(0);
channel.subscribe(FINAL_HANDLER);
assertThat(channel.subscribers()).isEqualTo(1);
assertThat(dispatcher.getHandlerCount()).isEqualTo(1);
channel.subscribe(FIRST_HANDLER);
assertThat(channel.subscribers()).isEqualTo(2);
assertThat(dispatcher.getHandlerCount()).isEqualTo(2);
channel.subscribe(SECOND_HANDLER);
assertThat(channel.subscribers()).isEqualTo(3);
assertThat(dispatcher.getHandlerCount()).isEqualTo(3);
channel.unsubscribe(FIRST_HANDLER);
assertThat(channel.subscribers()).isEqualTo(2);
assertThat(dispatcher.getHandlerCount()).isEqualTo(2);
channel.unsubscribe(FINAL_HANDLER);
assertThat(channel.subscribers()).isEqualTo(1);
assertThat(dispatcher.getHandlerCount()).isEqualTo(1);
channel.unsubscribe(SECOND_HANDLER);
assertThat(channel.subscribers()).isEqualTo(0);
assertThat(dispatcher.getHandlerCount()).isEqualTo(0);
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2016-2022 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 org.springframework.cloud.stream.binder;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Marius Bogoevici
* @author Oleg Zhurakousky
*/
public class ErrorBindingTests {
@Test
public void testConfigurationWithDefaultErrorHandler() {
ApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ErrorBindingTests.ErrorConfigurationDefault.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
"--spring.cloud.function.definition=handle",
"--spring.cloud.stream.default.error-handler-definition=errorHandler",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
ErrorConfigurationDefault errorConfiguration = context
.getBean(ErrorConfigurationDefault.class);
assertThat(errorConfiguration.counter).isEqualTo(6);
}
@Test
void testConfigurationWithBindingSpecificErrorHandler() {
ApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ErrorBindingTests.ErrorConfigurationWithCustomErrorHandler.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.handle-in-0.consumer.max-attempts=1",
"--spring.cloud.function.definition=handle",
"--spring.cloud.stream.bindings.handle-in-0.error-handler-definition=errorHandler",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
source.send(new GenericMessage<byte[]>("Hello".getBytes()));
ErrorConfigurationWithCustomErrorHandler errorConfiguration = context
.getBean(ErrorConfigurationWithCustomErrorHandler.class);
assertThat(errorConfiguration.counter).isEqualTo(6);
}
@EnableAutoConfiguration
public static class TestProcessor {
@Bean
public Function<String, String> processor() {
return s -> s;
}
}
@EnableAutoConfiguration
public static class ErrorConfigurationDefault {
private int counter;
@Bean
public Function<String, String> handle() {
return v -> {
this.counter++;
throw new RuntimeException("Intentional");
};
}
@Bean
public Consumer<Object> errorHandler() {
return v -> {
this.counter++;
};
}
}
@EnableAutoConfiguration
public static class ErrorConfigurationWithCustomErrorHandler {
private int counter;
@Bean
public Function<String, String> handle() {
return v -> {
this.counter++;
throw new RuntimeException("Intentional");
};
}
@Bean
public Consumer<Object> errorHandler() {
return v -> {
this.counter++;
};
}
}
}

View File

@@ -0,0 +1,612 @@
/*
* Copyright 2018-2022 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 org.springframework.cloud.stream.binder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.MessageConverterConfigurer;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.Lifecycle;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.acks.AcknowledgmentCallback.Status;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.core.MessageSource;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.messaging.converter.SmartMessageConverter;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* @author Gary Russell
* @author Oleg Zhurakousky
* @author David Turanski
* @since 2.0
*
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class PollableConsumerTests {
private ApplicationContext context;
private SmartMessageConverter messageConverter;
@BeforeAll
public void before() {
this.messageConverter = new CompositeMessageConverterFactory()
.getMessageConverterForAllRegistered();
}
@Test
void testDefaultMessageSource() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(2);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
AtomicInteger count = new AtomicInteger();
assertThat(pollableSource.poll(message -> {
assertThat(message).isNotNull();
count.incrementAndGet();
})).isTrue();
assertThat(count.get()).isOne();
}
@Test
void testSimple() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder
.withPayload(((String) message.getPayload()).toUpperCase())
.copyHeaders(message.getHeaders()).build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(2);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicInteger count = new AtomicInteger();
assertThat(pollableSource.poll(received -> {
assertThat(received.getPayload()).isEqualTo("POLLED DATA");
assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeType.valueOf("text/plain"));
if (count.incrementAndGet() == 1) {
throw new RuntimeException("test retry");
}
})).isTrue();
assertThat(count.get()).isEqualTo(2);
}
@Test
void testConvertSimple() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
binder.setMessageSourceDelegate(
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes()));
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(1);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicReference<Object> payload = new AtomicReference<>();
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<Foo>() {
})).isTrue();
assertThat(payload.get()).isInstanceOf(Foo.class);
assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar");
// test the cache for coverage
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<Foo>() {
})).isTrue();
assertThat(payload.get()).isInstanceOf(Foo.class);
assertThat(((Foo) payload.get()).getFoo()).isEqualTo("bar");
}
@Test
void testConvertSimpler() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
BindingServiceProperties bsps = this.context
.getBean(BindingServiceProperties.class);
BindingProperties props = new BindingProperties();
props.setContentType("text/plain");
bsps.setBindings(Collections.singletonMap("foo", props));
binder.setMessageSourceDelegate(() -> new GenericMessage<>("foo".getBytes()));
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(1);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicReference<Object> payload = new AtomicReference<>();
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<String>() {
})).isTrue();
assertThat(payload.get()).isInstanceOf(String.class);
assertThat(payload.get()).isEqualTo("foo");
// test the cache for coverage
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<String>() {
})).isTrue();
assertThat(payload.get()).isInstanceOf(String.class);
assertThat(payload.get()).isEqualTo("foo");
}
@Test
void testConvertList() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
binder.setMessageSourceDelegate(() -> new GenericMessage<>(
"[{\"foo\":\"bar\"},{\"foo\":\"baz\"}]".getBytes()));
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(1);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicReference<Object> payload = new AtomicReference<>();
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<List<Foo>>() {
})).isTrue();
@SuppressWarnings("unchecked")
List<Foo> list = (List<Foo>) payload.get();
assertThat(list.size()).isEqualTo(2);
assertThat(list.get(0).getFoo()).isEqualTo("bar");
assertThat(list.get(1).getFoo()).isEqualTo("baz");
}
@Test
void testConvertMap() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
binder.setMessageSourceDelegate(
() -> new GenericMessage<>("{\"qux\":{\"foo\":\"bar\"}}".getBytes()));
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(1);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicReference<Object> payload = new AtomicReference<>();
assertThat(pollableSource.poll(received -> {
payload.set(received.getPayload());
}, new ParameterizedTypeReference<Map<String, Foo>>() {
})).isTrue();
@SuppressWarnings("unchecked")
Map<String, Foo> map = (Map<String, Foo>) payload.get();
assertThat(map.size()).isEqualTo(1);
assertThat(map.get("qux").getFoo()).isEqualTo("bar");
}
@Test
void testEmbedded() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
binder.setMessageSourceDelegate(() -> {
MessageValues original = new MessageValues("foo".getBytes(),
Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
"application/octet-stream"));
byte[] payload = new byte[0];
try {
payload = EmbeddedHeaderUtils.embedHeaders(original,
MessageHeaders.CONTENT_TYPE);
}
catch (Exception e) {
fail(e.getMessage());
}
return new GenericMessage<>(payload);
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setHeaderMode(HeaderMode.embeddedHeaders);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder
.withPayload(
new String((byte[]) message.getPayload()).toUpperCase())
.copyHeaders(message.getHeaders()).build();
}
});
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
assertThat(pollableSource.poll(received -> {
assertThat(received.getPayload()).isEqualTo("FOO");
assertThat(received.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo("application/octet-stream");
})).isTrue();
}
@Test
void testErrors() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder
.withPayload(((String) message.getPayload()).toUpperCase())
.copyHeaders(message.getHeaders()).build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(2);
properties.setBackOffInitialInterval(0);
properties.getRetryableExceptions().put(IllegalStateException.class, false);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final CountDownLatch latch = new CountDownLatch(1);
this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME,
SubscribableChannel.class).subscribe(m -> {
latch.countDown();
});
final AtomicInteger count = new AtomicInteger();
assertThat(pollableSource.poll(received -> {
count.incrementAndGet();
throw new RuntimeException("test recoverer");
})).isTrue();
assertThat(count.get()).isEqualTo(2);
Message<?> lastError = binder.getLastError();
assertThat(lastError).isNotNull();
assertThat(((Exception) lastError.getPayload()).getCause().getMessage())
.isEqualTo("test recoverer");
assertThat(pollableSource.poll(received -> {
count.incrementAndGet();
throw new IllegalStateException("no retries");
})).isTrue();
assertThat(count.get()).isEqualTo(3);
lastError = binder.getLastError();
assertThat(lastError).isNotNull();
assertThat(((Exception) lastError.getPayload()).getCause().getMessage())
.isEqualTo("no retries");
}
@Test
void testErrorsNoRetry() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder
.withPayload(((String) message.getPayload()).toUpperCase())
.copyHeaders(message.getHeaders()).build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setMaxAttempts(1);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final CountDownLatch latch = new CountDownLatch(1);
this.context.getBean(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME,
SubscribableChannel.class).subscribe(m -> {
latch.countDown();
});
final AtomicInteger count = new AtomicInteger();
assertThat(pollableSource.poll(received -> {
count.incrementAndGet();
throw new RuntimeException("test recoverer");
})).isTrue();
assertThat(count.get()).isEqualTo(1);
}
@Test
void testRequeue() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class);
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message)
.setHeader(
IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK,
callback)
.build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
properties.setMaxAttempts(2);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicInteger count = new AtomicInteger();
try {
assertThat(pollableSource.poll(received -> {
count.incrementAndGet();
throw new RequeueCurrentMessageException("test retry");
})).isTrue();
}
catch (Exception e) {
// no op
}
assertThat(count.get()).isEqualTo(2);
verify(callback).acknowledge(Status.REQUEUE);
}
@Test
void testRequeueWithNoAcknowledgementCallback() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message)
.build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
properties.setMaxAttempts(2);
properties.setBackOffInitialInterval(0);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
final AtomicInteger count = new AtomicInteger();
assertThat(pollableSource.poll(received -> {
count.incrementAndGet();
throw new RequeueCurrentMessageException("test retry");
})).isTrue();
assertThat(count.get()).isEqualTo(2);
}
@Test
void testRequeueFromErrorFlow() {
TestChannelBinder binder = createBinder();
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
AcknowledgmentCallback callback = mock(AcknowledgmentCallback.class);
pollableSource.addInterceptor(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message)
.setHeader(
IntegrationMessageHeaderAccessor.ACKNOWLEDGMENT_CALLBACK,
callback)
.build();
}
});
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(null);
properties.setMaxAttempts(1);
binder.bindPollableConsumer("foo", "bar", pollableSource, properties);
SubscribableChannel errorChannel = new DirectChannel();
errorChannel.subscribe(msg -> {
throw new RequeueCurrentMessageException((Throwable) msg.getPayload());
});
pollableSource.setErrorChannel(errorChannel);
try {
pollableSource.poll(received -> {
throw new RuntimeException("test requeue from error flow");
});
}
catch (Exception e) {
// no op
}
verify(callback).acknowledge(Status.REQUEUE);
}
@SuppressWarnings("unchecked")
@Test
void testAutoStartupOff() {
TestChannelBinder binder = createBinder();
binder.setMessageSourceDelegate(new LifecycleMessageSource(
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes())));
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setAutoStartup(false);
Binding<PollableSource<MessageHandler>> pollableSourceBinding = binder
.bindPollableConsumer("foo", "bar", pollableSource, properties);
assertThat(pollableSourceBinding.isRunning()).isFalse();
}
@SuppressWarnings("unchecked")
@Test
void testAutoStartupOn() {
TestChannelBinder binder = createBinder();
binder.setMessageSourceDelegate(new LifecycleMessageSource(
() -> new GenericMessage<>("{\"foo\":\"bar\"}".getBytes())));
MessageConverterConfigurer configurer = this.context
.getBean(MessageConverterConfigurer.class);
DefaultPollableMessageSource pollableSource = new DefaultPollableMessageSource(
this.messageConverter);
configurer.configurePolledMessageSource(pollableSource, "foo");
ExtendedConsumerProperties<Object> properties = new ExtendedConsumerProperties<>(
null);
properties.setAutoStartup(true);
Binding<PollableSource<MessageHandler>> pollableSourceBinding = binder
.bindPollableConsumer("foo", "bar", pollableSource, properties);
assertThat(pollableSourceBinding.isRunning()).isTrue();
}
private TestChannelBinder createBinder(String... args) {
this.context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration())
.web(WebApplicationType.NONE).run(args);
TestChannelBinder binder = this.context.getBean(TestChannelBinder.class);
return binder;
}
public static class Foo {
private String foo;
protected String getFoo() {
return this.foo;
}
protected void setFoo(String foo) {
this.foo = foo;
}
}
public static class LifecycleMessageSource<T> implements MessageSource<T>, Lifecycle {
private final MessageSource<T> delegate;
private boolean running = false;
public LifecycleMessageSource(MessageSource<T> delegate) {
this.delegate = delegate;
}
@Override
public Message<T> receive() {
return this.delegate.receive();
}
@Override
public void start() {
this.running = true;
}
@Override
public void stop() {
this.running = false;
}
@Override
public boolean isRunning() {
return this.running;
}
}
}

View File

@@ -0,0 +1,830 @@
/*
* Copyright 2017-2022 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 org.springframework.cloud.stream.binder.tck;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.json.JsonMapper;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Sort of a TCK test suite to validate payload conversion is done properly by interacting
* with binder's input/output destinations instead of its bridged channels. This means
* that all payloads (sent/received) must be expressed in the wire format (byte[])
*
* @author Oleg Zhurakousky
* @author Gary Russell
*
*/
public class ContentTypeTckTests {
@Test
void stringToMapMessageStreamListener() {
ApplicationContext context = new SpringApplicationBuilder(
StringToMapMessageConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload())).isEqualTo("oleg");
}
@Test
void pojoToPojo() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void pojoToString() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToStringConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("oleg");
}
@Test
void pojoToStringOutboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToStringConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("oleg");
}
@Test
void pojoToByteArray() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("oleg");
}
@Test
void pojoToByteArrayOutboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToByteArrayConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("oleg");
}
@Test
void stringToPojoInboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
StringToPojoConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-on-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessToPojoInboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessToPojoInboundContentTypeBindingJson() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-in-0.contentType=application/json",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessMessageToPojoInboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessMessageToPojoInboundContentTypeBindingJson() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessMessageToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.echo-in-0.contentType=application/json",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessToPojoWithTextHeaderContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
.setHeader(MessageHeaders.CONTENT_TYPE, MimeType.valueOf("text/plain"))
.build());
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void typelessToPojoOutboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessToMessageConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-out-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
.setHeader("contentType", new MimeType("text", "plain")).build());
Message<byte[]> outputMessage = target.receive();
System.out.println(new String(outputMessage.getPayload()));
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
assertThat(outputMessage.getPayload())
.isEqualTo(jsonPayload.getBytes());
}
@Test
void outboundMessageWithTextContentTypeOnly() {
ApplicationContext context = new SpringApplicationBuilder(
TypelessToMessageTextOnlyContentTypeConfiguration.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(MessageBuilder.withPayload(jsonPayload.getBytes())
.setHeader("contentType", new MimeType("text")).build());
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString())
.startsWith("text/");
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void stringToPojoInboundContentTypeHeader() {
ApplicationContext context = new SpringApplicationBuilder(
StringToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes(),
new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.TEXT_PLAIN))));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void byteArrayToPojoInboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
ByteArrayToPojoConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.echo-in-0.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void byteArrayToPojoInboundContentTypeHeader() {
ApplicationContext context = new SpringApplicationBuilder(
StringToPojoConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes(),
new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE,
MimeTypeUtils.TEXT_PLAIN))));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.APPLICATION_JSON_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void byteArrayToByteArray() {
ApplicationContext context = new SpringApplicationBuilder(
ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void byteArrayToByteArrayInboundOutboundContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
ByteArrayToByteArrayConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.cloud.stream.bindings.input.contentType=text/plain",
"--spring.cloud.stream.bindings.output.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void pojoMessageToStringMessage() {
ApplicationContext context = new SpringApplicationBuilder(
PojoMessageToStringMessageConfiguration.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeTypeUtils.TEXT_PLAIN_VALUE);
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("oleg");
}
@Test
void customMessageConverter_defaultContentTypeBinding() {
ApplicationContext context = new SpringApplicationBuilder(
StringToStringConfiguration.class, CustomConverters.class)
.web(WebApplicationType.NONE)
.run("--spring.cloud.stream.default.contentType=foo/bar",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage).isNotNull();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo("FooBarMessageConverter");
assertThat(outputMessage.getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo("foo/bar");
}
// Failure tests
@Test
void _jsonToPojoWrongDefaultContentTypeProperty() {
ApplicationContext context = new SpringApplicationBuilder(
PojoToPojoConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.default.contentType=text/plain",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
assertThat(binder.getLastError().getPayload() instanceof MessagingException)
.isTrue();
}
@Test
@Disabled
void _toStringDefaultContentTypePropertyUnknownContentType() {
ApplicationContext context = new SpringApplicationBuilder(
StringToStringConfiguration.class).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.default.contentType=foo/bar",
"--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
assertThat(
binder.getLastError().getPayload() instanceof MessageConversionException)
.isTrue();
}
@Test
void toCollectionWithParameterizedType() {
ApplicationContext context = new SpringApplicationBuilder(
CollectionWithParameterizedTypes.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "[{\"person\":{\"name\":\"jon\"},\"id\":123},{\"person\":{\"name\":\"jane\"},\"id\":456}]";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(outputMessage.getPayload()).isEqualTo(jsonPayload.getBytes());
}
// ======
@Test
void testWithMapInputParameter() {
ApplicationContext context = new SpringApplicationBuilder(
MapInputConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void testWithMapPayloadParameter() {
ApplicationContext context = new SpringApplicationBuilder(
MapInputConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
void testWithListInputParameter() {
ApplicationContext context = new SpringApplicationBuilder(
ListInputConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false", "--debug");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "[\"foo\",\"bar\"]";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isEqualTo(jsonPayload);
}
@Test
@Disabled // TODO fix it. We can recognize MessageHeaders and parse it out of the message properly
void testWithMessageHeadersInputParameter() {
ApplicationContext context = new SpringApplicationBuilder(
MessageHeadersInputConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
OutputDestination target = context.getBean(OutputDestination.class);
String jsonPayload = "{\"name\":\"oleg\"}";
source.send(new GenericMessage<>(jsonPayload.getBytes()));
Message<byte[]> outputMessage = target.receive();
assertThat(new String(outputMessage.getPayload(), StandardCharsets.UTF_8))
.isNotEqualTo(jsonPayload);
assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.ID)).isTrue();
assertThat(outputMessage.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE))
.isTrue();
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class CollectionWithParameterizedTypes {
@Bean
public Function<List<Employee<Person>>, List<Employee<Person>>> echo() {
return value -> {
assertThat(value.get(0) != null).isTrue();
return value;
};
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class PojoToPojoConfiguration {
@Bean
public Function<Person, Person> ecgo() {
return value -> value;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class PojoToStringConfiguration {
@Bean
public Function<Person, String> echo() {
return value -> value.toString();
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class PojoToByteArrayConfiguration {
@Bean
public Function<Person, byte[]> echo() {
return value -> value.toString().getBytes(StandardCharsets.UTF_8);
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class ByteArrayToPojoConfiguration {
@Bean
public Function<byte[], Person> echo(JsonMapper mapper) {
return value -> mapper.fromJson(value, Person.class);
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class StringToPojoConfiguration {
@Bean
public Function<String, Person> echo(JsonMapper mapper) {
return value -> mapper.fromJson(value, Person.class);
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class TypelessToPojoConfiguration {
@Bean
public Function<Object, Person> echo(JsonMapper mapper) {
return value -> value instanceof byte[]
? mapper.fromJson((byte[]) value, Person.class)
: mapper.fromJson((String) value, Person.class);
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class TypelessMessageToPojoConfiguration {
@Bean
public Function<Message<?>, Person> echo(JsonMapper mapper) {
return message -> message.getPayload() instanceof byte[]
? mapper.fromJson((byte[]) message.getPayload(), Person.class)
: mapper.fromJson((String) message.getPayload(), Person.class);
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class TypelessToMessageConfiguration {
@Bean
public Function<Object, Message<?>> echo() {
return value -> MessageBuilder.withPayload(value.toString())
.setHeader("contentType", new MimeType("text", "plain")).build();
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class TypelessToMessageTextOnlyContentTypeConfiguration {
@Bean
public Function<?, Message<?>> echo() {
return value -> {
return MessageBuilder.withPayload(value.toString())
.setHeader("expected-content-type", "text/*").build();
};
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class ByteArrayToByteArrayConfiguration {
@Bean
public Function<byte[], byte[]> echo() {
return value -> value;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class StringToStringConfiguration {
@Bean
public Function<String, String> echo() {
return v -> v;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class StringToMapMessageConfiguration {
@Bean
public Function<Message<Map<?, ?>>, String> echo() {
return value -> {
assertThat(value.getPayload() instanceof Map).isTrue();
return (String) value.getPayload().get("name");
};
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class PojoMessageToStringMessageConfiguration {
@Bean
public Function<Message<Person>, Message<String>> echo() {
return value -> MessageBuilder.withPayload(value.getPayload().toString())
.setHeader("expected-content-type", MimeTypeUtils.TEXT_PLAIN_VALUE)
.build();
}
}
public static class Employee<P> {
private P person;
private int id;
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
public P getPerson() {
return this.person;
}
public void setPerson(P person) {
this.person = person;
}
}
public static class Person {
private String name;
public Person() {
this(null);
}
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}
@Configuration(proxyBeanMethods = false)
public static class CustomConverters {
@Bean
public FooBarMessageConverter fooBarMessageConverter() {
return new FooBarMessageConverter(MimeType.valueOf("foo/bar"));
}
@Bean
public AlwaysStringKryoMessageConverter kryoOverrideMessageConverter() {
return new AlwaysStringKryoMessageConverter(
MimeType.valueOf("application/x-java-object"));
}
/**
* Even though this MessageConverter has nothing to do with Kryo it still shows
* how Kryo conversion can be customized/overriden since it simply overriding a
* converter for contentType 'application/x-java-object'.
*
*/
public static class AlwaysStringKryoMessageConverter
extends AbstractMessageConverter {
public AlwaysStringKryoMessageConverter(MimeType supportedMimeType) {
super(supportedMimeType);
}
@Override
protected boolean supports(Class<?> clazz) {
return clazz == null || String.class.isAssignableFrom(clazz);
}
@Override
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
@Nullable Object conversionHint) {
return this.getClass().getSimpleName();
}
@Override
protected Object convertToInternal(Object payload,
@Nullable MessageHeaders headers, @Nullable Object conversionHint) {
return ((String) payload).getBytes(StandardCharsets.UTF_8);
}
}
public static class FooBarMessageConverter extends AbstractMessageConverter {
protected FooBarMessageConverter(MimeType supportedMimeType) {
super(supportedMimeType);
}
@Override
protected boolean supports(Class<?> clazz) {
return clazz != null && String.class.isAssignableFrom(clazz);
}
@Override
protected Object convertFromInternal(Message<?> message, Class<?> targetClass,
@Nullable Object conversionHint) {
return this.getClass().getSimpleName();
}
@Override
protected Object convertToInternal(Object payload,
@Nullable MessageHeaders headers, @Nullable Object conversionHint) {
return ((String) payload).getBytes(StandardCharsets.UTF_8);
}
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class MapInputConfiguration {
@Bean
public Function<Map<?, ?>, Map<?, ?>> echo() {
return value -> value;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class ListInputConfiguration {
@Bean
public Function<List<?>, List<?>> echo() {
return v -> v;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class MessageHeadersInputConfiguration {
@Bean
public Function<MessageHeaders, Map<?, ?>> echo() {
return v -> v;
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.binder.tck;
import java.util.function.Function;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
*
*/
@Disabled
public class ErrorHandlingTests {
@Test
void testGlobalErrorWithMessage() {
ApplicationContext context = new SpringApplicationBuilder(GlobalErrorHandlerWithErrorMessageConfig.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<>("foo".getBytes()));
GlobalErrorHandlerWithErrorMessageConfig config = context
.getBean(GlobalErrorHandlerWithErrorMessageConfig.class);
assertThat(config.globalErroInvoked).isTrue();
}
@Test
void testGlobalErrorWithThrowable() {
ApplicationContext context = new SpringApplicationBuilder(GlobalErrorHandlerWithThrowableConfig.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<>("foo".getBytes()));
GlobalErrorHandlerWithThrowableConfig config = context.getBean(GlobalErrorHandlerWithThrowableConfig.class);
assertThat(config.globalErroInvoked).isTrue();
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class GlobalErrorHandlerWithErrorMessageConfig {
private boolean globalErroInvoked;
@Bean
public Function<String, String> func() {
return v -> {
throw new RuntimeException("test exception");
};
}
@ServiceActivator(inputChannel = "errorChannel")
public void generalError(Message<?> message) {
this.globalErroInvoked = true;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class GlobalErrorHandlerWithThrowableConfig {
private boolean globalErroInvoked;
@Bean
public Function<String, String> func() {
return v -> {
throw new RuntimeException("test exception");
};
}
@ServiceActivator(inputChannel = "errorChannel")
public void generalError(Throwable exception) {
this.globalErroInvoked = true;
}
}
}

View File

@@ -0,0 +1,639 @@
/*
* Copyright 2015-2022 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 org.springframework.cloud.stream.binding;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.binder.BinderChildContextInitializer;
import org.springframework.cloud.stream.binder.BinderConfiguration;
import org.springframework.cloud.stream.binder.BinderFactory;
import org.springframework.cloud.stream.binder.BinderType;
import org.springframework.cloud.stream.binder.BinderTypeRegistry;
import org.springframework.cloud.stream.binder.Binding;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.DefaultBinderFactory;
import org.springframework.cloud.stream.binder.DefaultBinderTypeRegistry;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceConfiguration;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.reflection.GenericsUtils;
import org.springframework.cloud.stream.utils.IntegrationTestsMockBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* @author Gary Russell
* @author Mark Fisher
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Janne Valkealahti
* @author Soby Chacko
* @author Michael Michailidis
* @author Chris Bono
*/
public class BindingServiceTests {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
void testDefaultGroup() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo");
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding);
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
inputChannelName);
assertThat(bindings).hasSize(1);
Binding<MessageChannel> binding = bindings.iterator().next();
assertThat(binding).isSameAs(mockBinding);
service.unbindConsumers(inputChannelName);
verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel),
any(ConsumerProperties.class));
verify(binding).unbind();
binderFactory.destroy();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
void testMultipleConsumerBindings() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo,bar");
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class);
Binding<MessageChannel> mockBinding2 = Mockito.mock(Binding.class);
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding1);
when(binder.bindConsumer(eq("bar"), isNull(), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding2);
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
"input");
assertThat(bindings).hasSize(2);
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
Binding<MessageChannel> binding1 = iterator.next();
Binding<MessageChannel> binding2 = iterator.next();
assertThat(binding1).isSameAs(mockBinding1);
assertThat(binding2).isSameAs(mockBinding2);
service.unbindConsumers("input");
verify(binder).bindConsumer(eq("foo"), isNull(), same(inputChannel),
any(ConsumerProperties.class));
verify(binder).bindConsumer(eq("bar"), isNull(), same(inputChannel),
any(ConsumerProperties.class));
verify(binding1).unbind();
verify(binding2).unbind();
binderFactory.destroy();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
void testMultipleConsumerBindingsFromIndexList() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo");
ConsumerProperties consumer = properties.getConsumerProperties("input");
consumer.setInstanceIndexList(Arrays.asList(0, 1));
consumer.setInstanceCount(2);
consumer.setPartitioned(true);
props.setConsumer(consumer);
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class, "FirstBinding");
Binding<MessageChannel> mockBinding2 = Mockito.mock(Binding.class, "SecondBinding");
ArgumentCaptor<ConsumerProperties> captor = ArgumentCaptor.forClass(ConsumerProperties.class);
when(binder.bindConsumer(eq("foo"), isNull(), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding1).thenReturn(mockBinding2);
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
"input");
assertThat(bindings).hasSize(2);
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
Binding<MessageChannel> binding1 = iterator.next();
Binding<MessageChannel> binding2 = iterator.next();
assertThat(binding1).isSameAs(mockBinding1);
assertThat(binding2).isSameAs(mockBinding2);
service.unbindConsumers("input");
verify(binder, times(2)).bindConsumer(eq("foo"), isNull(), same(inputChannel),
captor.capture());
verify(binding1).unbind();
verify(binding2).unbind();
List<ConsumerProperties> allValues = captor.getAllValues();
assertThat(allValues.size()).isEqualTo(2);
assertThat(allValues.get(0).getInstanceIndex()).isEqualTo(0);
assertThat(allValues.get(1).getInstanceIndex()).isEqualTo(1);
binderFactory.destroy();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
void testConsumerBindingWhenMultiplexingIsEnabled() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo,bar");
ConsumerProperties consumer = properties.getConsumerProperties("input");
consumer.setMultiplex(true);
props.setConsumer(consumer);
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
Binding<MessageChannel> mockBinding1 = Mockito.mock(Binding.class);
when(binder.bindConsumer(eq("foo,bar"), isNull(), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding1);
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
"input");
assertThat(bindings).hasSize(1);
Iterator<Binding<MessageChannel>> iterator = bindings.iterator();
Binding<MessageChannel> binding1 = iterator.next();
assertThat(binding1).isSameAs(mockBinding1);
service.unbindConsumers("input");
verify(binder).bindConsumer(eq("foo,bar"), isNull(), same(inputChannel),
any(ConsumerProperties.class));
verify(binding1).unbind();
binderFactory.destroy();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
void testExplicitGroup() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo");
props.setGroup("fooGroup");
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
BindingService service = new BindingService(properties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
when(binder.bindConsumer(eq("foo"), eq("fooGroup"), same(inputChannel),
any(ConsumerProperties.class))).thenReturn(mockBinding);
Collection<Binding<MessageChannel>> bindings = service.bindConsumer(inputChannel,
inputChannelName);
assertThat(bindings).hasSize(1);
Binding<MessageChannel> binding = bindings.iterator().next();
assertThat(binding).isSameAs(mockBinding);
service.unbindConsumers(inputChannelName);
verify(binder).bindConsumer(eq("foo"), eq(props.getGroup()), same(inputChannel),
any(ConsumerProperties.class));
verify(binding).unbind();
binderFactory.destroy();
}
@Test
@Disabled
void testProducerPropertiesValidation() {
BindingServiceProperties serviceProperties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
ProducerProperties producerProperties = new ProducerProperties();
producerProperties.setPartitionCount(0);
props.setDestination("foo");
props.setProducer(producerProperties);
final String outputChannelName = "output";
bindingProperties.put(outputChannelName, props);
serviceProperties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper());
MessageChannel outputChannel = new DirectChannel();
try {
service.bindProducer(outputChannel, outputChannelName);
fail("Producer properties should be validated.");
}
catch (IllegalStateException e) {
assertThat(e)
.hasMessageContaining("Partition count should be greater than zero.");
}
}
@Test
void testDefaultPropertyBehavior() {
ConfigurableApplicationContext run = SpringApplication.run(
DefaultConsumerPropertiesTestSink.class,
"--server.port=0",
"--spring.cloud.stream.output-bindings=output1",
"--spring.cloud.stream.input-bindings=inputFooBarBuzz",
"--spring.cloud.stream.default.contentType=text/plain",
"--spring.cloud.stream.bindings.input1.contentType=application/json",
"--spring.cloud.stream.default.group=foo",
"--spring.cloud.stream.bindings.input2.group=bar",
"--spring.cloud.stream.default.consumer.concurrency=5",
"--spring.cloud.stream.bindings.input2.consumer.concurrency=1",
"--spring.cloud.stream.bindings.input1.consumer.partitioned=true",
"--spring.cloud.stream.default.producer.partitionCount=10",
"--spring.cloud.stream.bindings.output2.producer.partitionCount=1",
"--spring.cloud.stream.bindings.inputXyz.contentType=application/json",
"--spring.cloud.stream.bindings.inputFooBar.contentType=application/avro",
"--spring.cloud.stream.bindings.input_snake_case.contentType=application/avro");
BindingServiceProperties bindingServiceProperties = run.getBeanFactory()
.getBean(BindingServiceProperties.class);
Map<String, BindingProperties> bindings = bindingServiceProperties.getBindings();
assertThat(bindings.get("input1").getContentType()).isEqualTo("application/json");
assertThat(bindings.get("input2").getContentType()).isEqualTo("text/plain");
assertThat(bindings.get("input1").getGroup()).isEqualTo("foo");
assertThat(bindings.get("input2").getGroup()).isEqualTo("bar");
assertThat(bindings.get("input1").getConsumer().getConcurrency()).isEqualTo(5);
assertThat(bindings.get("input2").getConsumer().getConcurrency()).isEqualTo(1);
assertThat(bindings.get("input1").getConsumer().isPartitioned()).isEqualTo(true);
assertThat(bindings.get("input2").getConsumer().isPartitioned()).isEqualTo(false);
assertThat(bindings.get("output1-out-0").getProducer().getPartitionCount())
.isEqualTo(10);
assertThat(bindings.get("output2").getProducer().getPartitionCount())
.isEqualTo(1);
assertThat(bindings.get("inputXyz").getContentType())
.isEqualTo("application/json");
assertThat(bindings.get("inputFooBar").getContentType())
.isEqualTo("application/avro");
assertThat(bindings.get("inputFooBarBuzz-in-0").getContentType())
.isEqualTo("text/plain");
assertThat(bindings.get("input_snake_case").getContentType())
.isEqualTo("application/avro");
run.close();
}
@Test
@Disabled
void testConsumerPropertiesValidation() {
BindingServiceProperties serviceProperties = new BindingServiceProperties();
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
ConsumerProperties consumerProperties = new ConsumerProperties();
consumerProperties.setConcurrency(0);
props.setDestination("foo");
props.setConsumer(consumerProperties);
final String inputChannelName = "input";
bindingProperties.put(inputChannelName, props);
serviceProperties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
BindingService service = new BindingService(serviceProperties, binderFactory, new ObjectMapper());
MessageChannel inputChannel = new DirectChannel();
try {
service.bindConsumer(inputChannel, inputChannelName);
fail("Consumer properties should be validated.");
}
catch (IllegalStateException e) {
assertThat(e)
.hasMessageContaining("Concurrency should be greater than zero.");
}
}
@Test
void testUnknownBinderOnBindingFailure() {
HashMap<String, String> properties = new HashMap<>();
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
properties.put("spring.cloud.stream.bindings.input.binder", "mock");
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
properties.put("spring.cloud.stream.bindings.output.binder", "mockError");
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
properties);
BindingService bindingService = new BindingService(bindingServiceProperties,
createMockBinderFactory(), new ObjectMapper());
bindingService.bindConsumer(new DirectChannel(), "input");
try {
bindingService.bindProducer(new DirectChannel(), "output");
fail("Expected 'Unknown binder configuration'");
}
catch (IllegalStateException e) {
assertThat(e).hasMessageContaining("Unknown binder configuration: mockError");
}
}
@SuppressWarnings("unchecked")
@Test
void testUnrecognizedBinderAllowedIfNotUsed() {
HashMap<String, String> properties = new HashMap<>();
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
properties.put("spring.cloud.stream.defaultBinder", "mock1");
properties.put("spring.cloud.stream.binders.mock1.type", "mock");
properties.put("spring.cloud.stream.binders.kafka1.type", "kafka");
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
properties);
BinderFactory binderFactory = new BindingServiceConfiguration()
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class),
Mockito.mock(BinderChildContextInitializer.class));
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory, new ObjectMapper());
bindingService.bindConsumer(new DirectChannel(), "input");
bindingService.bindProducer(new DirectChannel(), "output");
}
@SuppressWarnings("unchecked")
@Test
void testUnrecognizedBinderDisallowedIfUsed() {
HashMap<String, String> properties = new HashMap<>();
properties.put("spring.cloud.stream.bindings.input.destination", "fooInput");
properties.put("spring.cloud.stream.bindings.input.binder", "mock1");
properties.put("spring.cloud.stream.bindings.output.destination", "fooOutput");
properties.put("spring.cloud.stream.bindings.output.type", "kafka1");
properties.put("spring.cloud.stream.binders.mock1.type", "mock");
properties.put("spring.cloud.stream.binders.kafka1.type", "kafka");
BindingServiceProperties bindingServiceProperties = createBindingServiceProperties(
properties);
BinderFactory binderFactory = new BindingServiceConfiguration()
.binderFactory(createMockBinderTypeRegistry(), bindingServiceProperties, Mockito.mock(ObjectProvider.class),
Mockito.mock(BinderChildContextInitializer.class));
BindingService bindingService = new BindingService(bindingServiceProperties,
binderFactory, new ObjectMapper());
bindingService.bindConsumer(new DirectChannel(), "input");
try {
bindingService.bindProducer(new DirectChannel(), "output");
fail("Expected 'Unknown binder configuration'");
}
catch (IllegalArgumentException e) {
assertThat(e).hasMessageContaining("Binder type kafka is not defined");
}
}
@Test
void testResolveBindableType() {
Class<?> bindableType = GenericsUtils.getParameterType(FooBinder.class,
Binder.class, 0);
assertThat(bindableType).isSameAs(SomeBindableType.class);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
void testLateBindingProducer() throws Exception {
BindingServiceProperties properties = new BindingServiceProperties();
properties.setBindingRetryInterval(1);
Map<String, BindingProperties> bindingProperties = new HashMap<>();
BindingProperties props = new BindingProperties();
props.setDestination("foo");
final String outputChannelName = "output";
bindingProperties.put(outputChannelName, props);
properties.setBindings(bindingProperties);
DefaultBinderFactory binderFactory = createMockBinderFactory();
Binder binder = binderFactory.getBinder("mock", MessageChannel.class);
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.initialize();
BindingService service = new BindingService(properties, binderFactory, scheduler, new ObjectMapper());
MessageChannel outputChannel = new DirectChannel();
final Binding<MessageChannel> mockBinding = Mockito.mock(Binding.class);
final CountDownLatch fail = new CountDownLatch(2);
doAnswer(i -> {
fail.countDown();
if (fail.getCount() == 1) {
throw new RuntimeException("fail");
}
return mockBinding;
}).when(binder).bindProducer(eq("foo"), same(outputChannel),
any(ProducerProperties.class));
Binding<MessageChannel> binding = service.bindProducer(outputChannel,
outputChannelName);
assertThat(fail.await(10, TimeUnit.SECONDS)).isTrue();
assertThat(binding).isNotNull();
Binding delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class);
int n = 0;
while (n++ < 300 && delegate == null) {
Thread.sleep(100);
delegate = TestUtils.getPropertyValue(binding, "delegate", Binding.class);
}
assertThat(delegate).isSameAs(mockBinding);
service.unbindProducers(outputChannelName);
verify(binder, times(2)).bindProducer(eq("foo"), same(outputChannel),
any(ProducerProperties.class));
verify(delegate).unbind();
binderFactory.destroy();
scheduler.destroy();
}
@SuppressWarnings("unchecked")
@Test
void testBindingAutostartup() throws Exception {
ApplicationContext context = new SpringApplicationBuilder(FooConfiguration.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.input-in-0.consumer.auto-startup=false");
BindingService bindingService = context.getBean(BindingService.class);
Field cbField = ReflectionUtils.findField(BindingService.class,
"consumerBindings");
cbField.setAccessible(true);
Map<String, Object> cbMap = (Map<String, Object>) cbField.get(bindingService);
Binding<?> inputBinding = ((List<Binding<?>>) cbMap.get("input-in-0")).get(0);
assertThat(inputBinding.isRunning()).isFalse();
}
@SuppressWarnings("unchecked")
@Test
void testBindingNameAsTopLevelProperty() throws Exception {
ApplicationContext context = new SpringApplicationBuilder(BarConfiguration.class)
.web(WebApplicationType.NONE).run();
final BindingServiceProperties bindingServiceProperties = context.getBean(BindingServiceProperties.class);
final ConsumerProperties consumerProperties = bindingServiceProperties.getConsumerProperties("myFunction-in-0");
assertThat(consumerProperties.getBindingName()).isEqualTo("myFunction-in-0");
final ProducerProperties producerProperties = bindingServiceProperties.getProducerProperties("myFunction-out-0");
assertThat(producerProperties.getBindingName()).isEqualTo("myFunction-out-0");
}
private DefaultBinderFactory createMockBinderFactory() {
BinderTypeRegistry binderTypeRegistry = createMockBinderTypeRegistry();
return new DefaultBinderFactory(
Collections.singletonMap("mock",
new BinderConfiguration("mock", new HashMap<>(), true, true)),
binderTypeRegistry, null);
}
private DefaultBinderTypeRegistry createMockBinderTypeRegistry() {
return new DefaultBinderTypeRegistry(Collections.singletonMap("mock",
new BinderType("mock", new Class[] { IntegrationTestsMockBinderConfiguration.class })));
}
private BindingServiceProperties createBindingServiceProperties(
HashMap<String, String> properties) {
BindingServiceProperties bindingServiceProperties = new BindingServiceProperties();
org.springframework.boot.context.properties.bind.Binder propertiesBinder;
propertiesBinder = new org.springframework.boot.context.properties.bind.Binder(
new MapConfigurationPropertySource(properties));
propertiesBinder.bind("spring.cloud.stream",
org.springframework.boot.context.properties.bind.Bindable
.ofInstance(bindingServiceProperties));
return bindingServiceProperties;
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class DefaultConsumerPropertiesTestSink {
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class FooConfiguration {
@Bean("input")
public Consumer<Message<?>> log() {
return System.out::println;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class BarConfiguration {
@Bean
public Function<String, String> myFunction() {
return s -> s;
}
}
public static class FooBinder
implements Binder<SomeBindableType, ConsumerProperties, ProducerProperties> {
@Override
public Binding<SomeBindableType> bindConsumer(String name, String group,
SomeBindableType inboundBindTarget,
ConsumerProperties consumerProperties) {
throw new UnsupportedOperationException();
}
@Override
public Binding<SomeBindableType> bindProducer(String name,
SomeBindableType outboundBindTarget,
ProducerProperties producerProperties) {
throw new UnsupportedOperationException();
}
}
public static class SomeBindableType {
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2022-2022 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 org.springframework.cloud.stream.binding;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.MessageChannel;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
*
*/
public class ExplicitBindingTests {
@Test
void testExplicitBindings() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.input-bindings=fooin;barin",
"--spring.cloud.stream.output-bindings=fooout;barout")) {
assertThat(context.getBean("fooin-in-0", MessageChannel.class)).isNotNull();
assertThat(context.getBean("barin-in-0", MessageChannel.class)).isNotNull();
assertThat(context.getBean("fooout-out-0", MessageChannel.class)).isNotNull();
assertThat(context.getBean("barout-out-0", MessageChannel.class)).isNotNull();
}
}
@Test
void testExplicitBindingsWithExistingConsumer() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ConsumerConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.output-bindings=consume")) {
assertThat(context.getBean("consume-in-0", MessageChannel.class)).isNotNull();
assertThat(context.getBean("consume-out-0", MessageChannel.class)).isNotNull();
}
}
@Disabled
@Test
void testExplicitBindingsWithExistingSupplier() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.input-bindings=supply",
"--spring.cloud.stream.bindings.supply-out-0.producer.poller.fixed-delay=3000")) {
assertThat(context.getBean("supply-in-0", MessageChannel.class)).isNotNull();
assertThat(context.getBean("supply-out-0", MessageChannel.class)).isNotNull();
OutputDestination output = context.getBean(OutputDestination.class);
assertThat(output.receive()).isNotNull();
assertThat(output.receive(500)).isNull();
assertThat(output.receive(500)).isNull();
assertThat(output.receive(500)).isNull();
assertThat(output.receive(500)).isNull();
assertThat(output.receive(500)).isNull();
assertThat(output.receive(500)).isNotNull();
}
}
@EnableAutoConfiguration
@Configuration
public static class EmptyConfiguration {
}
@EnableAutoConfiguration
@Configuration
public static class ConsumerConfiguration {
@Bean
public Consumer<String> consume() {
return System.out::println;
}
}
@EnableAutoConfiguration
@Configuration
public static class SupplierConfiguration {
@Bean
public Supplier<String> supply() {
return () -> "hello";
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2022-2022 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 org.springframework.cloud.stream.binding;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.channel.FluxMessageChannel;
import org.springframework.messaging.MessageChannel;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Soby Chacko
*/
public class FluxMessageChannelBindingTests {
@Test
void testFluxMessageChannelBindingWhenReactiveOptIn() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.reactive.uppercase=true")) {
assertThat(context.getBean("uppercase-in-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class);
assertThat(context.getBean("uppercase-out-0", MessageChannel.class)).isInstanceOf(FluxMessageChannel.class);
}
}
@EnableAutoConfiguration
@Configuration
public static class ReactiveFunctionConfiguration {
@Bean
public Function<Flux<String>, Flux<String>> uppercase() {
return s -> s.map(String::toUpperCase);
}
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2018-2022 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 org.springframework.cloud.stream.config;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.validation.annotation.Validated;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
// see https://github.com/spring-cloud/spring-cloud-stream/issues/1573 for more details
/**
* @author Oleg Zhurakousky
* @author Soby Chacko
*
*/
public class BindingHandlerAdviseTests {
@Test
@Disabled
void testFailureWithWrongValue() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(SampleConfiguration.class).web(WebApplicationType.NONE).run("--props.value=-1",
"--spring.jmx.enabled=false"));
}
@Test
void testValidatedValueValue() {
ValidatedProps validatedProps = new SpringApplicationBuilder(SampleConfiguration.class)
.web(WebApplicationType.NONE).run("--props.value=2", "--spring.jmx.enabled=false")
.getBean(ValidatedProps.class);
assertThat(validatedProps.getValue()).isEqualTo(2);
}
@Test
void nonValidatedConfigProperties() {
new SpringApplicationBuilder(NonValidatedConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
// simply should not fail
}
@Test
@Disabled
void validatedConfigProperties() {
assertThatExceptionOfType(ConfigurationPropertiesBindException.class)
.isThrownBy(() -> new SpringApplicationBuilder(ValidatedConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false"));
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class NonValidatedConfiguration {
@Bean
public Function<String, String> processor() {
return s -> s;
}
@Bean
@ConfigurationProperties
public NonValidatedClass nonValidatedClass() {
return new NonValidatedClass();
}
}
public static class NonValidatedClass {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class ValidatedConfiguration {
@Bean
public Function<String, String> processor() {
return s -> s;
}
@Bean
@ConfigurationProperties
public ValidatedClass nonValidatedClass() {
return new ValidatedClass();
}
}
@Validated
public static class ValidatedClass {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
@EnableConfigurationProperties(ValidatedProps.class)
class SampleConfiguration {
@Bean
public Consumer<String> sink() {
return System.out::println;
}
}
@ConfigurationProperties("props")
@Validated
class ValidatedProps {
private int value;
public int getValue() {
return this.value;
}
public void setValue(int value) {
this.value = value;
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2017-2022 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 org.springframework.cloud.stream.config;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
*
*/
public class BindingServiceConfigurationTests {
@Test
void testErroChannelDistributesMessagesInCaseOfException() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE).run()) {
AtomicInteger counter = new AtomicInteger();
PublishSubscribeChannel channel = context.getBean("errorChannel", PublishSubscribeChannel.class);
//channel.setIgnoreFailures(true);
channel.subscribe(m -> {
counter.incrementAndGet();
throw new RuntimeException("one");
});
channel.subscribe(m -> {
counter.incrementAndGet();
throw new RuntimeException("two");
});
channel.subscribe(m -> {
counter.incrementAndGet();
throw new RuntimeException("three");
});
channel.send(new GenericMessage<String>("foo"));
assertThat(counter.get()).isEqualTo(3);
}
}
@Test
void valdateImportedConfiguartionHandlerPostProcessing() {
ApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(RootConfiguration.class))
.web(WebApplicationType.NONE).run();
Map<String, AbstractReplyProducingMessageHandler> beansOfType = context
.getBeansOfType(AbstractReplyProducingMessageHandler.class);
for (AbstractReplyProducingMessageHandler handler : beansOfType.values()) {
assertThat(handler.getNotPropagatedHeaders().contains("contentType"))
.isTrue();
}
}
@Configuration
@Import(ImportedConfiguration.class)
public static class RootConfiguration {
@ServiceActivator(inputChannel = "input")
public void rootService(String val) {
}
}
@Configuration
public static class ImportedConfiguration {
@ServiceActivator(inputChannel = "input")
public void importedService(String val) {
}
}
@Configuration
public static class EmptyConfiguration {
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2018-2022 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 org.springframework.cloud.stream.config;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.annotation.StreamRetryTemplate;
import org.springframework.cloud.stream.binder.AbstractBinder;
import org.springframework.cloud.stream.binder.ConsumerProperties;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.retry.support.RetryTemplate;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Oleg Zhurakousky
* @author Soby Chacko
*/
public class RetryTemplateTests {
@SuppressWarnings("rawtypes")
@Test
void testSingleCustomRetryTemplate() throws Exception {
ApplicationContext context = new SpringApplicationBuilder(
SingleCustomRetryTemplateConfiguration.class).web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false");
AbstractBinder binder = context.getBean(AbstractBinder.class);
Field f = AbstractBinder.class.getDeclaredField("consumerBindingRetryTemplates");
f.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, RetryTemplate> consumerBindingRetryTemplates = (Map<String, RetryTemplate>) f
.get(binder);
assertThat(consumerBindingRetryTemplates).hasSize(1);
}
@SuppressWarnings("rawtypes")
@Test
void testSpecificCustomRetryTemplate() throws Exception {
ApplicationContext context = new SpringApplicationBuilder(
SpecificCustomRetryTemplateConfiguration.class)
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.processor-in-0.consumer.retry-template-name=retryTemplateTwo");
RetryTemplate retryTemplateTwo = context.getBean("retryTemplateTwo",
RetryTemplate.class);
BindingServiceProperties bindingServiceProperties = context
.getBean(BindingServiceProperties.class);
ConsumerProperties consumerProperties = bindingServiceProperties
.getConsumerProperties("processor-in-0");
AbstractBinder binder = context.getBean(AbstractBinder.class);
Method m = AbstractBinder.class.getDeclaredMethod("buildRetryTemplate",
ConsumerProperties.class);
m.setAccessible(true);
RetryTemplate retryTemplate = (RetryTemplate) m.invoke(binder,
consumerProperties);
assertThat(retryTemplate).isEqualTo(retryTemplateTwo);
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class SpecificCustomRetryTemplateConfiguration {
@StreamRetryTemplate
public RetryTemplate retryTemplate() {
return new RetryTemplate();
}
@StreamRetryTemplate
public RetryTemplate retryTemplateTwo() {
return new RetryTemplate();
}
@Bean
public RetryTemplate otherRetryTemplate() {
return new RetryTemplate();
}
@Bean
public Function<String, String> processor() {
return s -> s;
}
}
@Import(TestChannelBinderConfiguration.class)
@EnableAutoConfiguration
public static class SingleCustomRetryTemplateConfiguration {
@StreamRetryTemplate
public RetryTemplate retryTemplate() {
return new RetryTemplate();
}
@Bean
public RetryTemplate otherRetryTemplate() {
return new RetryTemplate();
}
@Bean
public Function<String, String> processor() {
return s -> s;
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2018-2021 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 org.springframework.cloud.stream.endpoint;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.BindingsLifecycleController;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
*/
public class ActuatorBindingsTest {
/*
* Even though this test performs some simple assertions, the main purpose for it is to validate that
* it does not result in recursive exception described in https://github.com/spring-cloud/spring-cloud-stream/issues/2253
*/
@Test
void test_2253() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(Bindings.class))
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=consume",
"--spring.jackson.visibility.field=ANY" // see https://github.com/spring-cloud/spring-cloud-stream/issues/2253
// we need the above just to verify that such action does not
// interfere with instance of ObjectMapper inside of BindingsLifecycleController
)) {
BindingsLifecycleController controller = context
.getBean(BindingsLifecycleController.class);
List<Map<?, ?>> bindings = controller.queryStates();
assertThat(bindings.size()).isEqualTo(1);
assertThat(bindings.get(0).get("bindingName")).isEqualTo("consume-in-0");
}
}
@EnableAutoConfiguration
public static class Bindings {
@Bean
public Consumer<String> consume() {
return message -> System.out.println("Received message " + message);
}
}
}

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.function;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Oleg Zhurakousky
* @author David Turanski
*
*
* TODO: Need to rewrite this test.
*/
public class DynamicDestinationFunctionTests {
@AfterAll
public static void after() {
System.clearProperty("spring.cloud.stream.function.definition");
System.clearProperty("spring.cloud.function.definition");
}
@Test
@Disabled
void testEmptyConfiguration() {
TestChannelBinderConfiguration.applicationContextRunner(SampleConfiguration.class)
.withPropertyValues(
"spring.jmx.enabled=false",
"spring.cloud.stream.bindings.fooDestination.producer.partitionKeyExtractorName=keyExtractor")
.run(context -> {
InputDestination input = context.getBean(InputDestination.class);
input.send(new GenericMessage<String>("fooDestination"));
BindingServiceProperties serviceProperties = context.getBean(BindingServiceProperties.class);
assertThat("keyExtractor").isEqualTo(
serviceProperties.getProducerProperties("fooDestination").getPartitionKeyExtractorName());
OutputDestination output = context.getBean(OutputDestination.class);
Object result = output.receive(1000).getPayload();
assertThat(result).isEqualTo("fooDestination");
});
}
@EnableAutoConfiguration
public static class SampleConfiguration {
// @Autowired
// private BinderAwareChannelResolver resolver;
@Bean
public PartitionKeyExtractorStrategy keyExtractor() {
return new PartitionKeyExtractorStrategy() {
@Override
public Object extractKey(Message<?> message) {
return 0;
}
};
}
// @Bean
// public Consumer<String> cons() {
// return value -> {
// resolver.resolveDestination(value).send(new GenericMessage<String>(value));
// };
// }
}
}

View File

@@ -0,0 +1,311 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.function;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.assertj.core.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.kafka.support.KafkaNull;
import org.springframework.messaging.Message;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Gary Russel
* @author Oleg Zhurakousky
* @author David Turanski
*
* @since 3.0
*/
public class FunctionBatchingTests {
@Test
void testMessageBatchConfigurationWithKafkaNull() {
TestChannelBinderConfiguration.applicationContextRunner(MessageBatchConfiguration.class)
.withPropertyValues("spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func",
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<Object> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
list.add(KafkaNull.INSTANCE);
list.add("{\"name\":\"steve\"}".getBytes());
Message<List<Object>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
});
}
@Test
void testListPayloadConfiguration() {
TestChannelBinderConfiguration.applicationContextRunner(ListPayloadNotBatchConfiguration.class)
.withPropertyValues("spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes())
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
});
}
@Test
void testListStringPayloadConfigurationTextPlain() {
TestChannelBinderConfiguration.applicationContextRunner(ListStringPayloadConfiguration.class)
.withPropertyValues("spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func",
"spring.cloud.stream.bindings.func-in-0.content-type=text/plain")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()});
Message inputMessage = MessageBuilder.withPayload(bytes).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]");
context.stop();
});
}
@Test
void testListObjectPayloadObjectConfigurationTextPlain() {
TestChannelBinderConfiguration.applicationContextRunner(ListObjectPayloadConfiguration.class)
.withPropertyValues("spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func",
"spring.cloud.stream.bindings.func-in-0.content-type=text/plain")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List bytes = Arrays.asList(new Object[] {"abc".getBytes(), "xyz".getBytes()});
Message inputMessage = MessageBuilder.withPayload(bytes).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(new String(outputMessage.getPayload())).isEqualTo("[abc, xyz]");
context.stop();
});
}
@Test
void testSimpleBatchConfiguration() {
TestChannelBinderConfiguration.applicationContextRunner(SimpleBatchConfiguration.class)
.withPropertyValues(
"spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func",
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("{\"name\":\"bob\"}".getBytes());
list.add("{\"name\":\"jill\"}".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
});
}
@Test
void testNestedBatchConfiguration() {
TestChannelBinderConfiguration.applicationContextRunner(NestedBatchConfiguration.class)
.withPropertyValues("spring.jmx.enabled=false",
"spring.cloud.stream.function.definition=func",
"spring.cloud.stream.bindings.input.consumer.batch-mode=true")
.run(context -> {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
List<byte[]> list = new ArrayList<>();
list.add("[{\"name\":\"bob\"},{\"name\":\"jill\"}]".getBytes());
Message<List<byte[]>> inputMessage = MessageBuilder
.withPayload(list)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage).isNotNull();
assertThat(outputMessage.getPayload())
.isEqualTo("{\"name\":\"bob\"}".getBytes());
context.stop();
});
}
@EnableAutoConfiguration
public static class SimpleBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class ListStringPayloadConfiguration {
@Bean
public Function<List<String>, String> func() {
return x -> x.toString();
}
}
@EnableAutoConfiguration
public static class ListObjectPayloadConfiguration {
@Bean
public Function<List<Object>, String> func() {
return x -> x.toString();
}
}
@EnableAutoConfiguration
public static class ListPayloadNotBatchConfiguration {
@Bean
public Function<List<Person>, Person> func() {
return x -> x.get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class NestedBatchConfiguration {
@Bean
public Function<List<List<Person>>, Person> func() {
return x -> x.get(0).get(0);
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
@EnableAutoConfiguration
public static class MessageBatchConfiguration {
@Bean
public Function<Message<List<Person>>, Person> func() {
return x -> {
Object o = x.getPayload().get(2);
assertThat(o).isNull();
return (Person) x.getPayload().get(0);
};
}
public static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2018-2022 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 org.springframework.cloud.stream.function;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
* This test validates proper function binding for applications where EnableBinding is
* declared.
*
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
public class GreenfieldFunctionEnableBindingTests {
@Test
void testProcessorFromFunction() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ProcessorFromFunction.class)).web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=toUpperCase",
"--spring.jmx.enabled=false")) {
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<byte[]>("John Doe".getBytes()));
OutputDestination target = context.getBean(OutputDestination.class);
assertThat(target.receive(10000).getPayload())
.isEqualTo("JOHN DOE".getBytes(StandardCharsets.UTF_8));
}
}
@Test
void testSinkFromConsumer() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration
.getCompleteConfiguration(SinkFromConsumer.class))
.web(WebApplicationType.NONE)
.run("--spring.cloud.function.definition=sink",
"--spring.jmx.enabled=false")) {
InputDestination source = context.getBean(InputDestination.class);
PollableChannel result = context.getBean("result", PollableChannel.class);
source.send(new GenericMessage<byte[]>("John Doe".getBytes()));
assertThat(result.receive(10000).getPayload()).isEqualTo("John Doe");
}
}
@EnableAutoConfiguration
public static class SourceFromSupplier {
@Bean
public Supplier<Date> date() {
return () -> new Date(12345L);
}
}
@EnableAutoConfiguration
public static class ProcessorFromFunction {
@Bean
public Function<String, String> toUpperCase() {
return String::toUpperCase;
}
}
@EnableAutoConfiguration
public static class SinkFromConsumer {
@Bean
public PollableChannel result() {
return new QueueChannel();
}
@Bean
public Consumer<String> sink(PollableChannel result) {
return s -> {
result.send(new GenericMessage<String>(s));
System.out.println(s);
};
}
}
static class Foo {
String bar;
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
}
}

View File

@@ -0,0 +1,488 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.function;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.UnicastProcessor;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.support.MessageBuilder;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
*
* @author Oleg Zhurakousky
*
*/
public class MultipleInputOutputFunctionTests {
@Test
void testFailureWithNonReactiveFunction() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multipleInputNonReactive"));
}
@Test
void testFailureWithReactiveArrayOutput() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutput"));
}
@Test
void testFailureWithReactiveArrayOutputNonGeneric() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutputNoGeneric"));
}
@Test
void testFailureWithReactiveArrayInput() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=genericReactiveArrayInput"));
}
@Test
void testFailureWithReactiveArrayInputNonGeneric() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=nonGenericReactiveArrayInput"));
}
@Test
void testFailureWithConsumer() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiInputConsumer"));
}
@Test
void testMultiInputSingleOutput() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiInputSingleOutput")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build();
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build();
inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0");
inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1");
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes());
outputMessage = outputDestination.receive(0, "multiInputSingleOutput-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes());
}
}
@Test
void testMultiInputMessageSingleOutput() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiInputSingleOutputMessage")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build();
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build();
inputDestination.send(stringInputMessage, "multiInputSingleOutputMessage-in-0");
inputDestination.send(integerInputMessage, "multiInputSingleOutputMessage-in-1");
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes());
outputMessage = outputDestination.receive(0, "multiInputSingleOutputMessage-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes());
}
}
@Test
void testSingleInputMultiOutput() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=singleInputMultipleOutputs")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
for (int i = 0; i < 10; i++) {
inputDestination.send(MessageBuilder.withPayload(String.valueOf(i).getBytes()).build(), "singleInputMultipleOutputs-in-0");
}
int counter = 0;
for (int i = 0; i < 5; i++) {
Message<byte[]> even = outputDestination.receive(0, "singleInputMultipleOutputs-out-0");
assertThat(even.getPayload()).isEqualTo(("EVEN: " + String.valueOf(counter++)).getBytes());
Message<byte[]> odd = outputDestination.receive(0, "singleInputMultipleOutputs-out-1");
assertThat(odd.getPayload()).isEqualTo(("ODD: " + String.valueOf(counter++)).getBytes());
}
}
}
@Test
void testMultipleFunctions() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=uppercase;reverse")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
inputDestination.send(inputMessage, "uppercase-in-0");
inputDestination.send(inputMessage, "reverse-in-0");
Message<byte[]> outputMessage = outputDestination.receive(0, "uppercase-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
outputMessage = outputDestination.receive(0, "reverse-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes());
}
}
@Test
void testMultipleFunctionsWithComposition() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ReactiveFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=uppercase|reverse;reverse|uppercase")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
inputDestination.send(inputMessage, "uppercasereverse-in-0");
inputDestination.send(inputMessage, "reverseuppercase-in-0");
Message<byte[]> outputMessage = outputDestination.receive(0, "uppercasereverse-out-0");
System.out.println(new String(outputMessage.getPayload()));
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
outputMessage = outputDestination.receive(0, "reverseuppercase-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
}
}
@Test
void testMultiInputSingleOutputWithCustomContentType() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ContentTypeConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiInputSingleOutput",
"--spring.cloud.stream.bindings.multiInputSingleOutput-in-0.content-type=string/person",
"--spring.cloud.stream.bindings.multiInputSingleOutput-in-1.content-type=string/employee")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build();
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build();
inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0");
inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1");
Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("RICKY".getBytes());
outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("BOBBY".getBytes());
}
}
@Test
void testMultiInputSingleOutputWithCustomContentType2() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
ContentTypeConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=multiInputSingleOutput2",
"--spring.cloud.stream.bindings.multiInputSingleOutput2-in-0.content-type=string/person",
"--spring.cloud.stream.bindings.multiInputSingleOutput2-in-1.content-type=string/employee",
"--spring.cloud.stream.bindings.multiInputSingleOutput2-out-0.content-type=string/person")) {
context.getBean(InputDestination.class);
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build();
Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build();
inputDestination.send(stringInputMessage, "multiInputSingleOutput2-in-0");
inputDestination.send(integerInputMessage, "multiInputSingleOutput2-in-1");
Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput2-out-0");
assertThat(outputMessage.getPayload()).isEqualTo("rickybobby".getBytes());
}
}
@EnableAutoConfiguration
public static class ReactiveFunctionConfiguration {
@Bean
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
@Bean
public Function<String, String> reverse() {
return value -> new StringBuilder(value).reverse().toString();
}
@Bean
public Function<Tuple2<String, String>, String> multipleInputNonReactive() { // not supported
return tuple -> null;
}
@Bean
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<?>[]> multiReactiveInputReactiveArrayOutput() { // not supported
return tuple -> null;
}
@Bean
public Consumer<Tuple2<Flux<String>, Flux<Integer>>> multiInputConsumer() { // not supported
return tuple -> System.out.println();
}
@SuppressWarnings("rawtypes")
@Bean
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux[]> multiReactiveInputReactiveArrayOutputNoGeneric() { // not supported
return tuple -> null;
}
@Bean
public Function<Flux<?>[], Tuple2<Flux<String>, Flux<Integer>>> genericReactiveArrayInput() { // not supported
return tuple -> null;
}
@SuppressWarnings("rawtypes")
@Bean
public Function<Flux[], Tuple2<Flux<String>, Flux<Integer>>> nonGenericReactiveArrayInput() { // not supported
return tuple -> null;
}
@Bean
public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<String>> multiInputSingleOutput() {
return tuple -> {
Flux<String> stringStream = tuple.getT1();
Flux<String> intStream = tuple.getT2().map(i -> String.valueOf(i));
return Flux.merge(stringStream, intStream);
};
}
@Bean
public Function<Tuple2<Flux<Message<String>>, Flux<Message<Integer>>>, Flux<String>> multiInputSingleOutputMessage() {
return tuple -> {
Flux<String> stringStream = tuple.getT1().map(m -> m.getPayload());
Flux<String> intStream = tuple.getT2().map(i -> {
int v = i.getPayload();
return String.valueOf(v);
});
return Flux.merge(stringStream, intStream);
};
}
@Bean
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Function<Flux<Integer>, Tuple2<Flux<String>, Flux<String>>> singleInputMultipleOutputs() {
return flux -> {
Flux<Integer> connectedFlux = flux.publish().autoConnect(2);
UnicastProcessor even = UnicastProcessor.create();
UnicastProcessor odd = UnicastProcessor.create();
Flux<Integer> evenFlux = connectedFlux.filter(number -> number % 2 == 0).doOnNext(number -> even.onNext("EVEN: " + number));
Flux<Integer> oddFlux = connectedFlux.filter(number -> number % 2 != 0).doOnNext(number -> odd.onNext("ODD: " + number));
return Tuples.of(Flux.from(even).doOnSubscribe(x -> evenFlux.subscribe()), Flux.from(odd).doOnSubscribe(x -> oddFlux.subscribe()));
};
}
}
@EnableAutoConfiguration
public static class ContentTypeConfiguration {
@Bean
public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<String>> multiInputSingleOutput() {
return tuple -> {
Flux<String> stringStream = tuple.getT1().map(p -> p.getName().toUpperCase());
Flux<String> intStream = tuple.getT2().map(p -> p.getName().toUpperCase());
return Flux.merge(stringStream, intStream);
};
}
@Bean
public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<Person>> multiInputSingleOutput2() {
return tuple -> {
return Flux.merge(tuple.getT1(), tuple.getT2()).buffer(Duration.ofMillis(1000)).map(list -> {
String personName = ((Person) list.get(0)).getName();
String employeeName = ((Employee) list.get(1)).getName();
return new Person(personName + employeeName);
});
};
}
@Bean
public MessageConverter stringToPersonConverter() {
return new AbstractMessageConverter(MimeType.valueOf("string/person")) {
@Override
protected boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
@Nullable
protected Object convertFromInternal(
Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8);
Person person = new Person(name);
return person;
}
@Override
@Nullable
protected Object convertToInternal(
Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
return ((Person) payload).getName().getBytes(StandardCharsets.UTF_8);
}
};
}
@Bean
public MessageConverter stringToEmployeeConverter() {
return new AbstractMessageConverter(MimeType.valueOf("string/employee")) {
@Override
protected boolean supports(Class<?> clazz) {
return Employee.class.isAssignableFrom(clazz);
}
@Override
@Nullable
protected Object convertFromInternal(
Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8);
Employee person = new Employee(name);
return person;
}
@Override
@Nullable
protected Object convertToInternal(
Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
return ((Employee) payload).getName().getBytes(StandardCharsets.UTF_8);
}
};
}
}
private static class Person {
private final String name;
Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
private static class Employee {
private final String name;
Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2020-2022 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 org.springframework.cloud.stream.function;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;
import org.springframework.cloud.stream.binder.DefaultPollableMessageSource;
import org.springframework.cloud.stream.binder.PollableMessageSource;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.scheduling.PollerMetadata;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.PeriodicTrigger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
*
* @author Oleg Zhurakousky
* @author Artem Bilan
*
*/
public class PollableSourceTests {
@Test
void testPollableSource() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.pollable-source=blah",
"--spring.integration.poller.cron=*/2 * * * * *",
"--spring.cloud.stream.poller.max-messages-per-poll=4")) {
DefaultPollableMessageSource pollableSource = (DefaultPollableMessageSource) context.getBean(PollableMessageSource.class);
pollableSource.poll(message -> assertThat(message.getPayload()).isNotNull());
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
assertThat(pollerMetadata.getTrigger()).isInstanceOf(CronTrigger.class);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "expression.expression"))
.isEqualTo("*/2 * * * * *");
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(4);
}
}
@Test
void testPollerDefaultFixedDelay() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
.web(WebApplicationType.NONE).run()) {
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(1));
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1);
}
}
@Test
void testPollerProvidedFixedDelayAndMaxMessagesPerPoll() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.integration.poller.fixed-delay=7s",
"--spring.integration.poller.max-messages-per-poll=13")) {
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(false);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofSeconds(7));
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(13);
}
}
@Test
void testNoPollerFixedDelayIfFixedRatePresent() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.integration.poller.fixed-rate=200")) {
PollerMetadata pollerMetadata = context.getBean(PollerMetadata.class);
assertThat(pollerMetadata.getTrigger()).isInstanceOf(PeriodicTrigger.class);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "fixedRate")).isEqualTo(true);
assertThat(TestUtils.getPropertyValue(pollerMetadata.getTrigger(), "period")).isEqualTo(Duration.ofMillis(200));
assertThat(pollerMetadata.getMaxMessagesPerPoll()).isEqualTo(1);
}
}
@Test
void testPollerMutualProperties() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(PollableAppSampleConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.integration.poller.fixed-rate=200",
"--spring.cloud.stream.poller.fixed-delay=300"))
.withRootCauseExactlyInstanceOf(MutuallyExclusiveConfigurationPropertiesException.class)
.withMessageContaining("are mutually exclusive");
}
@EnableAutoConfiguration
@Configuration
public static class PollableAppSampleConfiguration {
}
}

View File

@@ -0,0 +1,413 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.function;
import java.lang.reflect.Field;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.config.RoutingFunction;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinder;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.AbstractSubscribableChannel;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Oleg Zhurakousky
* @since 2.2.1
*/
public class RoutingFunctionTests {
@BeforeAll
public static void before() {
System.getProperties().remove("spring.cloud.function.routing.enabled");
System.getProperties().remove("spring.cloud.stream.function.definition");
System.getProperties().remove("spring.cloud.function.definition");
System.getProperties().remove("spring.cloud.function.routing-expression");
}
@Test
void testRoutingViaExplicitEnablingAndDefinitionHeader() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.setHeader(FunctionProperties.PREFIX + ".definition", "echo")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
}
}
@Test
void testRoutingViaExplicitEnablingAndRoutingExpressionProperty() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : null",
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
}
}
@Test
void testRoutingViaExplicitEnablingAndRoutingExpressionHeader() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.setHeader("spring.cloud.function.routing-expression", "'echo'")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("Hello".getBytes());
}
}
@Test
void testRoutingViaExplicitDefinitionAndDefinitionHeader() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=" + RoutingFunction.FUNCTION_NAME)) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.setHeader("spring.cloud.function.definition", "echo|uppercase")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
}
}
@Test
void testDefaultRoutingFunctionBindingFlux() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.setHeader("spring.cloud.function.definition", "echoFlux")
.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)
.build();
inputDestination.send(inputMessage);
TestChannelBinder binder = context.getBean(TestChannelBinder.class);
Throwable ex = ((Exception) binder.getLastError().getPayload()).getCause();
assertThat(ex).isInstanceOf(IllegalStateException.class);
assertThat(ex.getMessage()).isEqualTo("Routing to functions that return Publisher is not supported in the context of Spring Cloud Stream.");
}
}
@Test
void testPojoFunction() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=" + RoutingFunction.FUNCTION_NAME)) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("{\"name\":\"bob\"}".getBytes())
.setHeader("spring.cloud.function.definition", "pojoecho")
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("{\"name\":\"bob\"}".getBytes());
}
}
@Test
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitly() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME,
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
}
}
@Test
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitly() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME)) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());
}
}
@Test
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledExplicitlyAndMoreComposition() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse",
"--spring.cloud.stream.function.routing.enabled=true")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes())
.build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
}
}
@Test
void testExplicitRoutingFunctionBindingWithCompositionAndRoutingEnabledImplicitlyAndMoreComposition() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingFunctionConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.stream.function.definition=enrich|" + RoutingFunction.FUNCTION_NAME + "|reverse")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
OutputDestination outputDestination = context
.getBean(OutputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("Hello".getBytes()).build();
inputDestination.send(inputMessage);
Message<byte[]> outputMessage = outputDestination.receive();
assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes());
}
}
@SuppressWarnings("unchecked")
@Test
void testRoutingToConsumers() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
RoutingConsumerConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.routing-expression=headers['func_name']")) {
InputDestination inputDestination = context.getBean(InputDestination.class);
Message<byte[]> inputMessage = MessageBuilder
.withPayload("foo".getBytes())
.setHeader("func_name", "consume")
.build();
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Field chField = ReflectionUtils.findField(outputDestination.getClass(), "channels");
chField.setAccessible(true);
List<AbstractSubscribableChannel> outputChannels = (List<AbstractSubscribableChannel>) chField.get(outputDestination);
assertThat(outputChannels.isEmpty());
inputDestination.send(inputMessage);
assertThat(outputChannels.isEmpty());
inputMessage = MessageBuilder
.withPayload("foo".getBytes())
.setHeader("func_name", "echo")
.build();
inputDestination.send(inputMessage);
assertThat(outputChannels.size()).isEqualTo(1);
}
}
@EnableAutoConfiguration
public static class RoutingConsumerConfiguration {
@Bean
public Consumer<String> consume() {
return System.out::println;
}
@Bean
public Function<String, String> echo() {
return x -> x;
}
}
@EnableAutoConfiguration
public static class RoutingFunctionConfiguration {
@Bean
public Function<String, String> echo() {
return x -> {
System.out.println("===> echo");
return x;
};
}
@Bean
public Function<Person, Person> pojoecho() {
return x -> {
System.out.println("===> pojoecho");
return x;
};
}
@Bean
public Function<Flux<String>, Flux<String>> echoFlux() {
return flux -> flux.map(x -> {
System.out.println("===> echoFlux");
return x;
});
}
@Bean
public Function<Message<String>, Message<String>> enrich() {
return x -> {
System.out.println("===> enrich");
return MessageBuilder.withPayload(x.getPayload()).setHeader("spring.cloud.function.definition", "uppercase").build();
};
}
@Bean
public Function<String, String> uppercase() {
return x -> {
System.out.println("===> uppercase");
return x.toUpperCase();
};
}
@Bean
public Function<String, String> reverse() {
return x -> {
System.out.println("===> reverse");
return new StringBuilder(x).reverse().toString();
};
}
}
@SuppressWarnings("unused")
private static class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,202 @@
/*
* 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 org.springframework.cloud.stream.function;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.GenericMessage;
import static org.assertj.core.api.Assertions.assertThat;
/**
*
* @author Oleg Zhurakousky
*
*/
public class ScenarioTests {
@Test
void test2106() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=consume;echo",
"--spring.cloud.stream.bindings.consume-in-0.destination=input",
"--spring.cloud.stream.bindings.echo-in-0.destination=echoin",
"--spring.cloud.stream.bindings.echo-out-0.destination=echoout",
"--spring.jmx.enabled=false")) {
ConsumerConfiguration configuration = context.getBean(ConsumerConfiguration.class);
OutputDestination output = context.getBean(OutputDestination.class);
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("input", "destination");
bridge.send("input", "destination");
bridge.send("input", "destination");
bridge.send("consume-in-0", "hello");
bridge.send("consume-in-0", "hello");
bridge.send("consume-in-0", "hello");
bridge.send("echoin", "hello");
bridge.send("echoin", "hello");
bridge.send("echoin", "hello");
assertThat(configuration.destinationCounter).isEqualTo(3);
assertThat(configuration.bindingCounter).isEqualTo(3);
assertThat(output.receive(1000, "echoout")).isNotNull();
assertThat(output.receive(1000, "echoout")).isNotNull();
assertThat(output.receive(1000, "echoout")).isNotNull();
assertThat(output.receive(1000, "echoout")).isNull();
}
}
@Test
void testComposingSupplierWuthTypelessMessageFunction() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(SupplierConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=messageSupplier|messageFunction")) {
OutputDestination output = context.getBean(OutputDestination.class);
assertThat(output.receive(1000, "messageSuppliermessageFunction-out-0")).isNotNull();
assertThat(output.receive(1200, "messageSuppliermessageFunction-out-0")).isNotNull();
assertThat(output.receive(1300, "messageSuppliermessageFunction-out-0")).isNotNull();
}
}
@Test
void test2107() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(FunctionReturningNullConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=uppercase")) {
InputDestination input = context.getBean(InputDestination.class);
input.send(new GenericMessage<byte[]>("a".getBytes()), "uppercase-in-0");
OutputDestination output = context.getBean(OutputDestination.class);
assertThat(new String(output.receive(2000, "uppercase-out-0").getPayload())).isEqualTo("a");
input.send(new GenericMessage<byte[]>("b".getBytes()), "uppercase-in-0");
assertThat(output.receive(2000, "uppercase-out-0")).isNull();
}
}
@Test
void test2113() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(TestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=genericTypeFunction")) {
InputDestination input = context.getBean(InputDestination.class);
OutputDestination output = context.getBean(OutputDestination.class);
input.send(new GenericMessage<byte[]>("hello".getBytes()), "genericTypeFunction-in-0");
assertThat(new String(output.receive(1000, "genericTypeFunction-out-0").getPayload())).isEqualTo("hello_hello");
}
}
@EnableAutoConfiguration
@Configuration
public static class TestConfiguration {
@SuppressWarnings("unchecked")
@Bean
public <I, O> Function<I, O> genericTypeFunction() {
return v -> {
return (O) ("hello_" + new String((byte[]) v));
};
}
}
@EnableAutoConfiguration
@Configuration
public static class SupplierConfiguration {
@Bean
public Supplier<Message<?>> messageSupplier() {
return () -> new GenericMessage<>("10/27/20 07:20:01");
}
@Bean
public Function<Message<?>, Message<?>> messageFunction() {
return message -> {
return message;
};
}
}
@EnableAutoConfiguration
@Configuration
public static class FunctionReturningNullConfiguration {
@Bean
public Function<String, String> uppercase() {
return v -> {
if ("a".equals(v)) {
return v;
}
else {
return null;
}
};
}
}
@EnableAutoConfiguration
public static class ConsumerConfiguration {
private int destinationCounter;
private int bindingCounter;
@Bean
public Consumer<String> consume() {
return v -> {
if (v.equals("destination")) {
destinationCounter++;
}
else {
bindingCounter++;
}
};
}
@Bean
public Function<String, String> echo() {
return v -> v;
}
}
}

View File

@@ -0,0 +1,810 @@
/*
* Copyright 2020-2022 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 org.springframework.cloud.stream.function;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.context.message.MessageUtils;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.cloud.stream.binding.BindingService;
import org.springframework.cloud.stream.binding.NewDestinationBindingCallback;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.config.GlobalChannelInterceptor;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.handler.LoggingHandler;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
*
* @author Oleg Zhurakousky
* @author Soby Chacko
*
*/
public class StreamBridgeTests {
@BeforeAll
public static void before() {
System.clearProperty("spring.cloud.function.definition");
}
@Test
void test_SCF_856() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
StreamBridge streamBridge = context.getBean(StreamBridge.class);
streamBridge.send("myBinding-out-0",
CloudEventMessageBuilder.withData("hello").setSource("my-source")
.setId(UUID.randomUUID().toString()).setSpecVersion("1.0").setType("myType")
.setHeader(MessageUtils.TARGET_PROTOCOL, "kafka").build(),
MimeTypeUtils.APPLICATION_JSON);
OutputDestination output = context.getBean(OutputDestination.class);
Message<byte[]> result = output.receive();
assertThat(result.getHeaders().get("ce_type")).isNotNull();
assertThat(result.getHeaders().get("ce_source")).isNotNull();
}
}
/*
* This test must not result in exception stating "Partition key cannot be null"
* See https://github.com/spring-cloud/spring-cloud-stream/issues/2249 for more details
*/
@Test
void test_2249() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(
EmptyConfiguration.class)).web(WebApplicationType.NONE).run(
"--spring.cloud.stream.source=outputA;outputB",
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3",
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']",
"--spring.cloud.stream.bindings.outputB-out-0.destination=outputB",
"--spring.cloud.stream.bindings.outputB-out-0.producer.partition-count=3",
"--spring.jmx.enabled=false")) {
StreamBridge streamBridge = context.getBean(StreamBridge.class);
streamBridge.send("outputA-out-0", MessageBuilder.withPayload("A").setHeader("partitionKey", "A").build());
streamBridge.send("outputB", MessageBuilder.withPayload("B").build());
streamBridge.send("outputA-out-0", MessageBuilder.withPayload("C").setHeader("partitionKey", "C").build());
streamBridge.send("outputB", MessageBuilder.withPayload("D").build());
OutputDestination output = context.getBean(OutputDestination.class);
assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue();
assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse();
assertThat(output.receive(1000, "outputA-out-0").getHeaders().containsKey("scst_partition")).isTrue();
assertThat(output.receive(1000, "outputB").getHeaders().containsKey("scst_partition")).isFalse();
}
}
/*
* This test verifies that when a partition key expression is set, then scst_partition is always set, even in
* concurrent scenarios.
* See https://github.com/spring-cloud/spring-cloud-stream/issues/2299 for more details
*/
@Test
void test_2299_scstPartitionAlwaysSetEvenInConcurrentScenarios() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(EmptyConfiguration.class)).web(
WebApplicationType.NONE).run("--spring.cloud.stream.source=outputA",
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-count=3",
"--spring.cloud.stream.bindings.outputA-out-0.producer.partition-key-expression=headers['partitionKey']",
"--spring.jmx.enabled=false")) {
StreamBridge streamBridge = context.getBean(StreamBridge.class);
int threadCount = 10;
Set<Thread> threads = IntStream.range(0, threadCount)
.mapToObj(i -> (Runnable) () -> IntStream.range(0, 100).forEach(j -> {
String value = "M-" + i + "-" + j;
streamBridge.send("outputA-out-0",
MessageBuilder.withPayload(value).setHeader("partitionKey", value).build());
})).map(Thread::new).collect(Collectors.toSet());
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
int messagesWithoutScstPartition = 0;
OutputDestination output = context.getBean(OutputDestination.class);
Message<byte[]> message = output.receive(1000, "outputA-out-0");
while (message != null) {
if (!message.getHeaders().containsKey("scst_partition")) {
messagesWithoutScstPartition++;
}
message = output.receive(1000, "outputA-out-0");
}
assertThat(messagesWithoutScstPartition).isEqualTo(0);
}
}
@Test
void testWithOutputContentTypeWildCardBindings() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfigurationWithCustomConverters.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.stream.bindings.foo.content-type=application/*+foo ",
"--spring.cloud.stream.bindings.bar.content-type=application/*+non-registered-foo",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo", "hello foo");
bridge.send("bar", "hello bar");
OutputDestination output = context.getBean(OutputDestination.class);
assertThat(output.receive(1000, "foo").getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeType.valueOf("application/json+foo"));
assertThat(output.receive(1000, "bar").getHeaders().get(MessageHeaders.CONTENT_TYPE))
.isEqualTo(MimeType.valueOf("application/blahblah+non-registered-foo"));
}
}
@SuppressWarnings("unchecked")
@Test
void testNoCachingOfStreamBridgeFunction() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=function",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_PLAIN);
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.APPLICATION_JSON);
bridge.send("function-in-0", (Object) "hello foo", MimeTypeUtils.TEXT_HTML);
Field field = ReflectionUtils.findField(StreamBridge.class, "streamBridgeFunctionCache");
field.setAccessible(true);
Map<String, FunctionInvocationWrapper> map = (Map<String, FunctionInvocationWrapper>) field.get(bridge);
assertThat(map.size()).isEqualTo(3);
}
}
@Test
void testDelayedSend() {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class, EmptyConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
executor.schedule(() -> bridge.send("blah", "hello foo"), 5000, TimeUnit.MILLISECONDS);
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(10000, "blah");
assertThat(message).isNotNull();
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
}
finally {
executor.shutdownNow();
}
}
@Test
void testWithInterceptorsMatchedAgainstAllPatterns() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class, InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=function",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("function-in-0", "hello foo");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "function-out-0");
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
assertThat(message.getHeaders().get("intercepted")).isEqualTo("true");
}
}
@Test // validate that there is no exception thrown when sending to null channel
void test_2268() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("nullChannel", "blah");
}
}
@Test
void testInterceptorIsNotAddedMultipleTimesToTheMessageChannel() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.dynamic-destination-cache-size=1",
"--spring.cloud.stream.output-bindings=outputA;outputB",
"--spring.cloud.stream.bindings.outputA-out-0.destination=outputA",
"--spring.cloud.stream.bindings.outputB-out-0.destination=outputB"
)) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("outputA-out-0", "hello foo");
bridge.send("outputA-out-0", "hello foo");
bridge.send("outputA-out-0", "hello foo");
bridge.send("outputA-out-0", "hello foo");
AbstractMessageChannel messageChannel = context.getBean("outputA-out-0", AbstractMessageChannel.class);
assertThat(messageChannel.getInterceptors()).hasSize(1);
}
}
@Test
void testBindingsAreRemovedWithCache() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(InterceptorConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false",
"--spring.cloud.stream.dynamic-destination-cache-size=1"
)) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("a", "hello foo");
bridge.send("b", "hello foo");
bridge.send("c", "hello foo");
bridge.send("d", "hello foo");
BindingService bindingService = context.getBean(BindingService.class);
assertThat(bindingService.getProducerBindingNames().length).isEqualTo(1);
assertThat(bindingService.getProducerBindingNames()[0]).isEqualTo("d");
}
}
@Test
void testWithInterceptorsRegisteredOnlyOnOutputChannel() throws InterruptedException {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(GH2180Configuration.class))
.web(WebApplicationType.NONE).run(
"--spring.jmx.enabled=false")) {
MessageChannel inputChannel = context.getBean("inputChannel", MessageChannel.class);
inputChannel.send(MessageBuilder.withPayload("hello foo").build());
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "outgoing-out-0");
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
assertThat(message.getHeaders().get("intercepted")).isEqualTo("true");
//Ensure that the LoggingHandler in the first SI flow is invoked.
GH2180Configuration.LATCH1.await(10, TimeUnit.SECONDS);
//Ensure that the second SI flow does not trigger its LoggingHandler (aka wiretap/interceptor).
assertThat(GH2180Configuration.LATCH2.getCount()).isEqualTo(1);
}
}
@Test
void testBindingPropertiesAreHonored() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(ConsumerConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=consumer;function",
"--spring.jmx.enabled=false",
"--spring.cloud.stream.bindings.foo.destination=function-in-0",
"--spring.cloud.stream.bindings.foo.producer.partitionCount=5",
"--spring.cloud.stream.bindings.foo.consumer.concurrency=2")) {
BindingServiceProperties bsProperties = context.getBean(BindingServiceProperties.class);
assertThat(bsProperties.getConsumerProperties("foo").getConcurrency()).isEqualTo(2);
assertThat(bsProperties.getProducerProperties("foo").getPartitionCount()).isEqualTo(5);
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("consumer-in-0", "hello foo");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "function-out-0");
assertThat(new String(message.getPayload())).isEqualTo("hello foo");
assertThat(message.getHeaders().get("concurrency")).isEqualTo(2);
assertThat(message.getHeaders().get("partitionCount")).isEqualTo(5);
}
}
//see https://github.com/spring-cloud/spring-cloud-function/issues/573 for more details
@Test
void testBridgeActivationWhenFunctionDefinitionIsPresent() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(SimpleConfiguration.class))
.web(WebApplicationType.NONE).run(
"--spring.cloud.function.definition=echo;uppercase",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("echo-in-0", "hello foo");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
assertThat(new String(outputDestination.receive(100, "echo-out-0").getPayload())).isEqualTo("hello foo");
}
}
@Test
void testNoBridgeIfNoSourcePropertyDefined() {
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> {
ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration())
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false");
context.getBean(StreamBridge.class);
});
}
@Test
void testBridgeFunctions() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo-out-0", "hello foo");
bridge.send("bar-out-0", "hello bar");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
assertThat(new String(outputDestination.receive(100, "foo-out-0").getPayload())).isEqualTo("hello foo");
assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("hello bar");
}
}
@Test
void testBridgeFunctionsSendingMessagePreservingHeaders() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo-out-0", MessageBuilder.withPayload("hello foo").setHeader("foo", "foo").build());
bridge.send("bar-out-0", MessageBuilder.withPayload("hello bar").setHeader("bar", "bar").build());
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<?> message = outputDestination.receive(100, "foo-out-0");
assertThat(message.getPayload()).isEqualTo("hello foo".getBytes());
assertThat(message.getHeaders().get("foo")).isEqualTo("foo");
message = outputDestination.receive(100, "bar-out-0");
assertThat(message.getPayload()).isEqualTo("hello bar".getBytes());
assertThat(message.getHeaders().get("bar")).isEqualTo("bar");
}
}
@Test
void testBridgeFunctionsWitthPartitionInformation() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(EmptyConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=foo;bar",
"--spring.cloud.stream.bindings.foo-out-0.producer.partitionKeyExpression=payload",
"--spring.cloud.stream.bindings.foo-out-0.producer.partitionCount=5",
"--spring.cloud.stream.bindings.bar-out-0.producer.partitionKeyExpression=payload",
"--spring.cloud.stream.bindings.bar-out-0.producer.partitionCount=1",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo-out-0", "a");
bridge.send("bar-out-0", "b");
bridge.send("foo-out-0", "c");
bridge.send("foo-out-0", "d");
bridge.send("bar-out-0", "e");
bridge.send("foo-out-0", "f");
bridge.send("bar-out-0", "g");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("a");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2);
message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("c");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(4);
message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("d");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
message = outputDestination.receive(100, "bar-out-0");
assertThat(new String(message.getPayload())).isEqualTo("b");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
message = outputDestination.receive(100, "bar-out-0");
assertThat(new String(message.getPayload())).isEqualTo("e");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
message = outputDestination.receive(100, "bar-out-0");
assertThat(new String(message.getPayload())).isEqualTo("g");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(0);
message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("f");
assertThat(message.getHeaders().get("scst_partition")).isEqualTo(2);
//assertThat(new String(outputDestination.receive(100, "bar-out-0").getPayload())).isEqualTo("b");
}
}
@Test
void testSendingMessageToOutputOfExistingSupplier() throws Exception {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(TestConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=supplier;foo",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("supplier-out-0", "blah");
bridge.send("foo-out-0", "b");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("b");
message = outputDestination.receive(100, "supplier-out-0");
assertThat(new String(message.getPayload())).isEqualTo("hello");
message = outputDestination.receive(100, "supplier-out-0");
assertThat(new String(message.getPayload())).isEqualTo("blah");
}
}
@Test
void testDynamicDestination() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(TestConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
OutputDestination outputDestination = context.getBean(OutputDestination.class);
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo-out-0", "b");
bridge.send("bar", "hello");
bridge.send("blah", MessageBuilder.withPayload("message").setHeader("foo", "foo").build());
Message<byte[]> message = outputDestination.receive(100, "foo-out-0");
assertThat(new String(message.getPayload())).isEqualTo("b");
message = outputDestination.receive(100, "bar");
assertThat(new String(message.getPayload())).isEqualTo("hello");
message = outputDestination.receive(100, "blah");
assertThat(new String(message.getPayload())).isEqualTo("message");
}
}
@Test
void testWithIntegrationFlowBecauseMarcinSaidSo() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(IntegrationFlowConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("foo", "blah");
OutputDestination outputDestination = context.getBean(OutputDestination.class);
Message<byte[]> message = outputDestination.receive(100, "output");
assertThat(new String(message.getPayload())).isEqualTo("BLAH");
}
}
@Test
void testNewBindingCallback() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(TestChannelBinderConfiguration
.getCompleteConfiguration(BindingCallbackConfiguration.class))
.web(WebApplicationType.NONE).run("--spring.cloud.stream.source=uppercase",
"--spring.jmx.enabled=false")) {
StreamBridge bridge = context.getBean(StreamBridge.class);
bridge.send("uppercase-in-0", "hello");
assertThat(context.getBean("callbackVerifier", AtomicBoolean.class)).isTrue();
}
}
@Test
void testDynamicProducerDestination() {
System.clearProperty("spring.cloud.function.definition");
ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(DynamicProducerDestinationConfig.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false",
"--spring.cloud.function.definition=uppercase",
"--spring.cloud.stream.bindings.uppercase-in-0.destination=upper"
);
InputDestination source = context.getBean(InputDestination.class);
source.send(new GenericMessage<>("John Doe".getBytes()), "upper");
OutputDestination target = context.getBean(OutputDestination.class);
Message<byte[]> message = target.receive(5, "dynamicTopic");
assertThat(message).isNotNull();
assertThat(new String(message.getPayload())).isEqualTo("JOHN DOE");
}
@EnableAutoConfiguration
public static class DynamicProducerDestinationConfig {
@Bean
public Function<Message<String>, Message<String>> uppercase() {
return msg -> MessageBuilder.withPayload(msg.getPayload().toUpperCase())
.setHeader("spring.cloud.stream.sendto.destination", "dynamicTopic").build();
}
}
@EnableAutoConfiguration
public static class EmptyConfiguration {
}
@EnableAutoConfiguration
public static class EmptyConfigurationWithCustomConverters {
@Bean
public MessageConverter fooConverter() {
return new AbstractMessageConverter(MimeType.valueOf("application/json+foo"), MimeType.valueOf("application/json+blah")) {
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
@Nullable
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
return message.getPayload();
}
@Override
@Nullable
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
if (headers.containsKey(MessageHeaders.CONTENT_TYPE) &&
(headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+foo") ||
headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+blah"))) {
return payload;
}
return null;
}
};
}
@Bean
public MessageConverter barConverter() {
return new AbstractMessageConverter(MimeType.valueOf("application/blahblah+non-registered-foo")) {
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
@Nullable
protected Object convertFromInternal(Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) {
return message.getPayload();
}
@Override
@Nullable
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) {
if (headers.containsKey(MessageHeaders.CONTENT_TYPE) &&
(headers.get(MessageHeaders.CONTENT_TYPE).toString().endsWith("+non-registered-foo"))) {
return payload;
}
return null;
}
};
}
}
@EnableAutoConfiguration
public static class ConsumerConfiguration {
@Bean
public Consumer<String> consumer(StreamBridge bridge, BindingServiceProperties properties) {
return v -> {
BindingServiceProperties p = properties;
bridge.send("foo", v);
};
}
@Bean
public Function<String, Message<String>> function(StreamBridge bridge, BindingServiceProperties properties) {
return v -> {
int concurrency = properties.getConsumerProperties("foo").getConcurrency();
int partitionCount = properties.getProducerProperties("foo").getPartitionCount();
BindingServiceProperties p = properties;
return MessageBuilder.withPayload(v)
.setHeader("concurrency", concurrency)
.setHeader("partitionCount", partitionCount)
.build();
};
}
}
@EnableAutoConfiguration
public static class InterceptorConfiguration {
@Bean
@GlobalChannelInterceptor(patterns = "*")
public ChannelInterceptor interceptor() {
return new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build();
}
};
}
}
@EnableAutoConfiguration
public static class TestConfiguration {
@Bean
public Supplier<String> supplier() {
return () -> "hello";
}
}
@EnableAutoConfiguration
public static class SimpleConfiguration {
@Bean
public Function<String, String> echo() {
return v -> v;
}
@Bean
public Function<String, String> uppercase() {
return v -> v.toUpperCase();
}
}
@EnableAutoConfiguration
public static class BindingCallbackConfiguration {
@Bean
public Function<String, String> echo() {
return v -> v;
}
@Bean
public Function<String, String> uppercase() {
return v -> v.toUpperCase();
}
@Bean
public AtomicBoolean callbackVerifier() {
return new AtomicBoolean();
}
@Bean
public NewDestinationBindingCallback callback(AtomicBoolean callbackVerifier) {
return (name, channel, props, extended) -> {
callbackVerifier.set(true);
};
}
}
@EnableAutoConfiguration
public static class IntegrationFlowConfiguration {
@Bean
public IntegrationFlow transform(StreamBridge bridge) {
return IntegrationFlows.from("foo").transform(v -> {
String s = new String((byte[]) v);
return s.toUpperCase();
})
.handle(v -> bridge.send("output", v))
.get();
}
}
@EnableAutoConfiguration
public static class GH2180Configuration {
static CountDownLatch LATCH1 = new CountDownLatch(1);
static CountDownLatch LATCH2 = new CountDownLatch(1);
@Bean
MessageChannel inputChannel() {
return new DirectChannel();
}
@Bean
MessageChannel otherInputChannel() {
return new DirectChannel();
}
@Bean
public IntegrationFlow someFlow(MessageHandler sendMessage, MessageChannel inputChannel) {
return IntegrationFlows.from(inputChannel)
.log(LoggingHandler.Level.INFO, (m) -> {
LATCH1.countDown();
return "Going through the first flow: " + m.getPayload();
})
.handle(sendMessage)
.get();
}
@Bean
public IntegrationFlow someOtherFlow(MessageHandler sendMessage) {
return IntegrationFlows.from(otherInputChannel())
.log(LoggingHandler.Level.INFO, (m) -> {
LATCH2.countDown();
return "Going through the second flow: " + m.getPayload();
})
.handle(sendMessage)
.get();
}
@Bean
@GlobalChannelInterceptor(patterns = "outgoing-*")
public ChannelInterceptor fooInterceptor() {
return new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
return MessageBuilder.fromMessage(message).setHeader("intercepted", "true").build();
}
};
}
@Bean
public MessageHandler sendMessage(StreamBridge streamBridge) {
return message -> {
streamBridge.send("outgoing-out-0", message);
};
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2019-2022 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 org.springframework.cloud.stream.function.edgecases;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
/**
* This test validates that the issue https://github.com/spring-cloud/spring-cloud-stream/issues/1801
* is addressed.
*
* @author Oleg Zhurakousky
*
*/
public class GH1801Test {
@Test
void test() {
SampleBootApplication.main("--spring.cloud.stream.defaultBinder=integration");
}
@SpringBootApplication
public static class SampleBootApplication {
public static void main(String... args) {
new SpringApplicationBuilder(SampleBootApplication.class).web(WebApplicationType.NONE).run(args);
}
}
}
@Configuration
class StreamConfiguration {
@Bean
public Consumer<Message<?>> consumer() {
return System.out::println;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2022-2022 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 org.springframework.cloud.stream.kotlin;
import org.junit.jupiter.api.Test;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.messaging.Message;
import static org.assertj.core.api.Assertions.assertThat;
public class KotlinConfigurationTests {
@Test
void testKotlinSupplierPollableBean() {
try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
TestChannelBinderConfiguration.getCompleteConfiguration(KotlinTestConfiguration.class))
.web(WebApplicationType.NONE)
.run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=produceNames")) {
OutputDestination output = context.getBean(OutputDestination.class);
Message<byte[]> result = output.receive(1000, "produceNames-out-0");
assertThat(result.getPayload()).isEqualTo("Ricky".getBytes());
result = output.receive(1000, "produceNames-out-0");
assertThat(result.getPayload()).isEqualTo("Julien".getBytes());
result = output.receive(1000, "produceNames-out-0");
assertThat(result.getPayload()).isEqualTo("Bubbles".getBytes());
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2015-2019 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 org.springframework.cloud.stream.utils;
import org.mockito.Mockito;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Marius Bogoevici
*/
@Configuration
public class IntegrationTestsMockBinderConfiguration {
@Bean
public Binder<?, ?, ?> binder() {
return Mockito.mock(Binder.class,
Mockito.withSettings().defaultAnswer(Mockito.RETURNS_MOCKS));
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2022-2022 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 org.springframework.cloud.stream.kotlin
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.cloud.function.context.PollableBean
import org.springframework.context.annotation.Configuration
import reactor.core.publisher.Flux
@Configuration
@EnableAutoConfiguration
open class KotlinTestConfiguration {
@PollableBean // it doesn't work with Kotlin lambda
open fun produceNames(): () -> Flux<String> = {
Flux.just(
"Ricky",
"Julien",
"Bubbles"
)
}
}