package io.smallrye.reactive.messaging.amqp; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; import static org.awaitility.Awaitility.await; import static org.hamcrest.core.Is.is; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.spi.ConnectorFactory; import org.eclipse.microprofile.reactive.streams.operators.SubscriberBuilder; import org.jboss.weld.environment.se.Weld; import org.jboss.weld.environment.se.WeldContainer; import org.jboss.weld.exceptions.DeploymentException; import org.junit.After; import org.junit.Ignore; import org.junit.Test; import org.reactivestreams.Subscriber; import io.smallrye.config.SmallRyeConfigProviderResolver; import io.smallrye.mutiny.Multi; import io.vertx.core.json.JsonObject; import repeat.Repeat; public class AmqpSinkTest extends AmqpTestBase { private static final String HELLO = "hello-"; private WeldContainer container; private AmqpConnector provider; @After public void cleanup() { if (provider != null) { provider.terminate(null); } if (container != null) { container.shutdown(); } MapBasedConfig.clear(); SmallRyeConfigProviderResolver.instance().releaseConfig(ConfigProvider.getConfig()); } @Test public void testSinkUsingInteger() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); usage.consumeIntegers(topic, v -> expected.getAndIncrement()); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(Message::of) .subscribe((Subscriber<? super Message<Integer>>) sink.build()); await().until(() -> expected.get() == 10); assertThat(expected).hasValue(10); } @Test public void testSinkUsingIntegerUsingNonAnonymousSender() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); usage.consumeIntegers(topic, v -> expected.getAndIncrement()); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndNonAnonymousSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(Message::of) .subscribe((Subscriber<? super Message<Integer>>) sink.build()); await().until(() -> expected.get() == 10); assertThat(expected).hasValue(10); } @Test public void testSinkUsingString() { String topic = UUID.randomUUID().toString(); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); AtomicInteger expected = new AtomicInteger(0); usage.consumeStrings(topic, v -> expected.getAndIncrement()); //noinspection unchecked Multi.createFrom().range(0, 10) .map(i -> Integer.toString(i)) .map(Message::of) .subscribe((Subscriber<? super Message<String>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); } static class Person { String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } @Test public void testSinkUsingObject() { String topic = UUID.randomUUID().toString(); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); AtomicInteger expected = new AtomicInteger(0); usage.consume(topic, v -> { expected.getAndIncrement(); Person p = v.bodyAsBinary().toJsonObject().mapTo(Person.class); assertThat(p.getName()).startsWith("bob-"); }); //noinspection unchecked Multi.createFrom().range(0, 10) .map(i -> { Person p = new Person(); p.setName("bob-" + i); return p; }) .map(Message::of) .subscribe((Subscriber<? super Message<Person>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); } @Test @Repeat(times = 3) public void testABeanProducingMessagesSentToAMQP() throws InterruptedException { Weld weld = new Weld(); CountDownLatch latch = new CountDownLatch(10); usage.consumeIntegers("sink", v -> latch.countDown()); weld.addBeanClass(ProducingBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .write(); container = weld.initialize(); assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); } @Test public void testABeanProducingMessagesSentToAMQPWithOutboundMetadata() throws InterruptedException { Weld weld = new Weld(); CountDownLatch latch = new CountDownLatch(10); usage.consumeIntegers("sink", v -> latch.countDown()); weld.addBeanClass(ProducingBeanUsingOutboundMetadata.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "not-used") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .write(); container = weld.initialize(); assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); } @Test public void testABeanProducingMessagesSentToAMQPWithOutboundMetadataUsingNonAnonymousSender() throws InterruptedException { Weld weld = new Weld(); CountDownLatch latch = new CountDownLatch(10); usage.consumeIntegers("sink-foo", v -> latch.countDown()); weld.addBeanClass(ProducingBeanUsingOutboundMetadata.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink-foo") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("mp.messaging.outgoing.sink.use-anonymous-sender", false) .put("amqp-username", username) .put("amqp-password", password) .write(); container = weld.initialize(); assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); } @Test public void testSinkUsingAmqpMessage() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); List<AmqpMessage<String>> messages = new ArrayList<>(); usage.consume(topic, v -> { expected.getAndIncrement(); v.getDelegate().accepted(); messages.add(new AmqpMessage<>(v, null, null)); }); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(v -> AmqpMessage.<String> builder() .withBody(HELLO + v) .withSubject("foo") .build()) .subscribe((Subscriber<? super AmqpMessage<String>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); messages.forEach(m -> { assertThat(m.getPayload()).isInstanceOf(String.class).startsWith(HELLO); assertThat(m.getSubject()).isEqualTo("foo"); }); } @Test public void testSinkUsingAmqpMessageWithNonAnonymousSender() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); List<AmqpMessage<String>> messages = new ArrayList<>(); usage.consume(topic, v -> { expected.getAndIncrement(); v.getDelegate().accepted(); messages.add(new AmqpMessage<>(v, null, null)); }); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndNonAnonymousSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(v -> AmqpMessage.<String> builder() .withBody(HELLO + v) .withSubject("foo") .withAddress("unused") .build()) .subscribe((Subscriber<? super AmqpMessage<String>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); messages.forEach(m -> { assertThat(m.getPayload()).isInstanceOf(String.class).startsWith(HELLO); assertThat(m.getSubject()).isEqualTo("foo"); }); } @Test public void testSinkUsingVertxAmqpMessage() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); List<AmqpMessage<String>> messages = new CopyOnWriteArrayList<>(); usage.consume(topic, v -> { expected.getAndIncrement(); messages.add(new AmqpMessage<>(v, null, null)); }); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(v -> io.vertx.mutiny.amqp.AmqpMessage.create() .withBody(HELLO + v) .subject("bar") .build()) .map(Message::of) .subscribe((Subscriber<? super Message<io.vertx.mutiny.amqp.AmqpMessage>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); messages.forEach(m -> { assertThat(m.getPayload()).isInstanceOf(String.class).startsWith(HELLO); assertThat(m.getSubject()).isEqualTo("bar"); }); } @Test public void testSinkUsingAmqpMessageAndChannelNameProperty() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); List<AmqpMessage<String>> messages = new ArrayList<>(); usage.consume(topic, v -> { expected.getAndIncrement(); messages.add(new AmqpMessage<>(v, null, null)); }); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSinkUsingChannelName(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(v -> AmqpMessage.<String> builder().withBody(HELLO + v).withSubject("foo").build()) .subscribe((Subscriber<? super AmqpMessage<String>>) sink.build()); await().untilAtomic(expected, is(10)); assertThat(expected).hasValue(10); messages.forEach(m -> { assertThat(m.getPayload()).isInstanceOf(String.class).startsWith(HELLO); assertThat(m.getSubject()).isEqualTo("foo"); }); } @Test(expected = DeploymentException.class) public void testConfigByCDIMissingBean() { Weld weld = new Weld(); weld.addBeanClass(ProducingBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("mp.messaging.outgoing.sink.client-options-name", "myclientoptions") .write(); container = weld.initialize(); } @Test(expected = DeploymentException.class) public void testConfigByCDIIncorrectBean() { Weld weld = new Weld(); weld.addBeanClass(ProducingBean.class); weld.addBeanClass(ClientConfigurationBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("mp.messaging.outgoing.sink.client-options-name", "dummyoptionsnonexistent") .write(); container = weld.initialize(); } @Test public void testConfigByCDICorrect() throws InterruptedException { Weld weld = new Weld(); CountDownLatch latch = new CountDownLatch(10); usage.consumeIntegers("sink", v -> latch.countDown()); weld.addBeanClass(ProducingBean.class); weld.addBeanClass(ClientConfigurationBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("mp.messaging.outgoing.sink.client-options-name", "myclientoptions") .write(); container = weld.initialize(); assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); } @Test(expected = DeploymentException.class) @Ignore("Failing on CI - need to be investigated") public void testConfigGlobalOptionsByCDIMissingBean() { Weld weld = new Weld(); weld.addBeanClass(ProducingBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("amqp-client-options-name", "dummyoptionsnonexistent") .write(); container = weld.initialize(); } @Test(expected = DeploymentException.class) @Ignore("Failing on CI - to be investigated") public void testConfigGlobalOptionsByCDIIncorrectBean() { Weld weld = new Weld(); weld.addBeanClass(ProducingBean.class); weld.addBeanClass(ClientConfigurationBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("amqp-client-options-name", "dummyoptionsnonexistent") .write(); container = weld.initialize(); } @Test public void testConfigGlobalOptionsByCDICorrect() throws InterruptedException { Weld weld = new Weld(); CountDownLatch latch = new CountDownLatch(10); usage.consumeIntegers("sink", v -> latch.countDown()); weld.addBeanClass(ProducingBean.class); weld.addBeanClass(ClientConfigurationBean.class); new MapBasedConfig() .put("mp.messaging.outgoing.sink.address", "sink") .put("mp.messaging.outgoing.sink.connector", AmqpConnector.CONNECTOR_NAME) .put("mp.messaging.outgoing.sink.host", host) .put("mp.messaging.outgoing.sink.port", port) .put("mp.messaging.outgoing.sink.durable", true) .put("amqp-username", username) .put("amqp-password", password) .put("amqp-client-options-name", "myclientoptions") .write(); container = weld.initialize(); assertThat(latch.await(1, TimeUnit.MINUTES)).isTrue(); } @Test public void testOutgoingMetadata() { String topic = UUID.randomUUID().toString(); List<io.vertx.mutiny.amqp.AmqpMessage> messages = new CopyOnWriteArrayList<>(); usage.consume(topic, messages::add); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); //noinspection unchecked Multi.createFrom().range(0, 10) .map(Message::of) .map(m -> m.addMetadata(OutgoingAmqpMetadata.builder() .withSubject("subject") .withPriority(2) .withGroupId("group") .withContentType("text/plain") .withProperties(new JsonObject().put("key", "value")) .withCorrelationId("correlation-" + m.getPayload()) .build())) .subscribe((Subscriber<? super Message<Integer>>) sink.build()); await().until(() -> messages.size() == 10); assertThat(messages).allSatisfy(msg -> { assertThat(msg.contentType()).isEqualTo("text/plain"); assertThat(msg.subject()).isEqualTo("subject"); assertThat(msg.priority()).isEqualTo((short) 2); assertThat(msg.correlationId()).startsWith("correlation-"); assertThat(msg.groupId()).isEqualTo("group"); assertThat(msg.applicationProperties()).containsExactly(entry("key", "value")); }); } @Test public void testCreditBasedFlowControl() { String topic = UUID.randomUUID().toString(); AtomicInteger expected = new AtomicInteger(0); usage.consumeIntegers(topic, v -> expected.getAndIncrement()); SubscriberBuilder<? extends Message<?>, Void> sink = createProviderAndSink(topic); //noinspection unchecked Multi.createFrom().range(0, 5000) .map(Message::of) .subscribe((Subscriber<? super Message<Integer>>) sink.build()); await().until(() -> expected.get() == 5000); assertThat(expected).hasValue(5000); } private SubscriberBuilder<? extends Message<?>, Void> createProviderAndSink(String topic) { Map<String, Object> config = new HashMap<>(); config.put(ConnectorFactory.CHANNEL_NAME_ATTRIBUTE, topic); config.put("address", topic); config.put("name", "the name"); config.put("host", host); config.put("durable", false); config.put("port", port); config.put("username", "artemis"); config.put("password", new String("simetraehcapa".getBytes())); this.provider = new AmqpConnector(); provider.setup(executionHolder); return this.provider.getSubscriberBuilder(new MapBasedConfig(config)); } private SubscriberBuilder<? extends Message<?>, Void> createProviderAndNonAnonymousSink(String topic) { Map<String, Object> config = new HashMap<>(); config.put(ConnectorFactory.CHANNEL_NAME_ATTRIBUTE, topic); config.put("address", topic); config.put("name", "the name"); config.put("host", host); config.put("durable", false); config.put("port", port); config.put("use-anonymous-sender", false); config.put("username", "artemis"); config.put("password", new String("simetraehcapa".getBytes())); this.provider = new AmqpConnector(); provider.setup(executionHolder); return this.provider.getSubscriberBuilder(new MapBasedConfig(config)); } private SubscriberBuilder<? extends Message<?>, Void> createProviderAndSinkUsingChannelName(String topic) { Map<String, Object> config = new HashMap<>(); config.put(ConnectorFactory.CHANNEL_NAME_ATTRIBUTE, topic); config.put("name", "the name"); config.put("host", host); config.put("durable", false); config.put("port", port); config.put("username", "artemis"); config.put("password", new String("simetraehcapa".getBytes())); this.provider = new AmqpConnector(); provider.setup(executionHolder); return this.provider.getSubscriberBuilder(new MapBasedConfig(config)); } }