Java tutorial
/* * Copyright (c) 2015-2016. Ventu.io, Oleg Sklyar and contributors, distributed under the MIT license. */ package io.ventu.rpc.amqp; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.AMQP.BasicProperties.Builder; import com.rabbitmq.client.AMQP.Queue.DeclareOk; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.Consumer; import com.rabbitmq.client.Envelope; import io.ventu.rpc.amqp.AmqpResponderImpl.ChannelProvider; import io.ventu.rpc.amqp.AmqpResponderImpl.ChannelProviderImpl; import io.ventu.rpc.amqp.defaults.DefaultRequestRouter; import io.ventu.rpc.amqp.defaults.DefaultSerializer; import io.ventu.rpc.exception.EncodingException; import io.ventu.rpc.util.RequestEvaluator; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import static io.ventu.rpc.RemoteInvoker.CONTENT_TYPE; import static io.ventu.rpc.RemoteInvoker.ENCODING; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; public class AmqpResponderImplTest { @Rule public ExpectedException exception = ExpectedException.none(); public static class Req { public int value; } public static class Res { public int value; } static class ReqEvaluator implements RequestEvaluator<Req, Res> { Map<String, Object> headers = null; Req request = null; @Override public CompletableFuture<Res> eval(Map<String, Object> headers, Req request) { this.headers = headers; this.request = request; CompletableFuture<Res> answer = new CompletableFuture<>(); if (request.value > 200) { Res res = new Res(); res.value = request.value * 2; answer.complete(res); } else { answer.completeExceptionally(new IndexOutOfBoundsException("boom")); } return answer; } } @Test public void constructor_callsProviderToGetChannel_success() throws TimeoutException, IOException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); assertSame(routingKeyPattern, responder.routingKeyPattern); assertSame(provider, responder.channelProvider); verify(provider).provide(routingKeyPattern, responder); verifyNoMoreInteractions(provider); } @Test public void handleDeliveryInternal_responsePayloadPublished() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; String correlationId = "123456789"; Channel channel = mock(Channel.class); ChannelProvider provider = mock(ChannelProvider.class); doReturn("replyExchange").when(provider).rpcExchange(); doReturn(channel).when(provider).provide(anyString(), any()); Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); final List<Boolean> invocations = Lists.newArrayList(); final CompletableFuture<byte[]> promise = new CompletableFuture<>(); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern) { @Override <RQ, RS> CompletableFuture<byte[]> handleDelivery(String routingKey, Map<String, Object> actualHeaders, byte[] payload) { invocations.add(Boolean.TRUE); assertEquals(Req.class.getName(), routingKey); assertEquals("ABC", new String(payload)); assertEquals(headers.get("apikey"), actualHeaders.get("apikey")); promise.complete("CBA".getBytes()); return promise; } }; Envelope env = new Envelope(1L, false, "incomingEexchange", Req.class.getName()); BasicProperties props = new Builder().correlationId(correlationId).replyTo("replyHere").headers(headers) .build(); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { invocations.add(Boolean.TRUE); assertEquals("replyExchange", invocation.getArguments()[0]); assertEquals("replyHere", invocation.getArguments()[1]); BasicProperties respProps = (BasicProperties) invocation.getArguments()[2]; assertEquals(correlationId, respProps.getCorrelationId()); assertEquals(CONTENT_TYPE, respProps.getContentType()); assertEquals(ENCODING, respProps.getContentEncoding()); assertEquals("CBA", new String((byte[]) invocation.getArguments()[3])); return null; } }).when(channel).basicPublish(anyString(), anyString(), any(), any()); responder.handleDeliveryInternal("consumerTag", env, props, "ABC".getBytes()).get(1, TimeUnit.SECONDS); verify(channel).basicPublish(anyString(), anyString(), any(), any()); verifyNoMoreInteractions(channel); assertEquals(2, invocations.size()); } @Test public void handleDeliveryInternal_publishException_convertedToISE() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; String correlationId = "123456789"; Channel channel = mock(Channel.class); ChannelProvider provider = mock(ChannelProvider.class); doReturn("replyExchange").when(provider).rpcExchange(); doReturn(channel).when(provider).provide(anyString(), any()); Map<String, Object> headers = Maps.newHashMap(); final CompletableFuture<byte[]> promise = new CompletableFuture<>(); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern) { @Override <RQ, RS> CompletableFuture<byte[]> handleDelivery(String routingKey, Map<String, Object> headers, byte[] payload) { promise.complete("CBA".getBytes()); return promise; } }; Envelope env = new Envelope(1L, false, "incomingEexchange", Req.class.getName()); BasicProperties props = new Builder().correlationId(correlationId).replyTo("replyHere").headers(headers) .build(); doThrow(new IOException("boom")).when(channel).basicPublish(anyString(), anyString(), any(), any()); CompletableFuture<Void> answer = responder.handleDeliveryInternal("consumerTag", env, props, "ABC".getBytes()); exception.expect(ExecutionException.class); try { answer.get(1, TimeUnit.SECONDS); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof IllegalStateException); throw ex; } } @Test public void handleDelivery_public_passThroughToHandleDeliveryInternal() throws TimeoutException, IOException { String routingKeyPattern = "routingKeyPattern"; final Envelope env = new Envelope(1L, false, "rpc", "io.ventu.#"); final BasicProperties props = new BasicProperties.Builder().build(); final byte[] data = new byte[] {}; final List<Boolean> invocations = Lists.newArrayList(); AmqpResponderImpl responder = new AmqpResponderImpl(mock(ChannelProvider.class), routingKeyPattern) { @Override CompletableFuture<Void> handleDeliveryInternal(String consumerTag, Envelope inEnv, BasicProperties inProps, byte[] inData) { invocations.add(Boolean.TRUE); assertSame(env, inEnv); assertSame(props, inProps); assertSame(data, inData); return null; } }; responder.handleDelivery("consumerTag", env, props, data); assertEquals(1, invocations.size()); } @Test public void handleDeliveryInternal_onPropagatedThrowable_apiErrorPublished() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; String correlationId = "123456789"; Channel channel = mock(Channel.class); ChannelProvider provider = mock(ChannelProvider.class); doReturn("replyExchange").when(provider).rpcExchange(); doReturn(channel).when(provider).provide(anyString(), any()); Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); final List<Boolean> invocations = Lists.newArrayList(); final CompletableFuture<byte[]> promise = new CompletableFuture<>(); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern) { @Override <RQ, RS> CompletableFuture<byte[]> handleDelivery(String routingKey, Map<String, Object> actualHeaders, byte[] payload) { invocations.add(Boolean.TRUE); promise.completeExceptionally(new IndexOutOfBoundsException("boom")); return promise; } }; Envelope env = new Envelope(1L, false, "incomingEexchange", Req.class.getName()); BasicProperties props = new Builder().correlationId(correlationId).replyTo("replyHere").headers(headers) .build(); doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { invocations.add(Boolean.TRUE); assertEquals("replyExchange", invocation.getArguments()[0]); assertEquals("replyHere", invocation.getArguments()[1]); BasicProperties respProps = (BasicProperties) invocation.getArguments()[2]; assertEquals(correlationId, respProps.getCorrelationId()); assertEquals(CONTENT_TYPE, respProps.getContentType()); assertEquals(ENCODING, respProps.getContentEncoding()); byte[] data = (byte[]) invocation.getArguments()[3]; Map<Object, Object> payload = new DefaultSerializer().decode(data, Map.class); assertEquals("Unhandled internal error: boom", payload.get("error")); return null; } }).when(channel).basicPublish(anyString(), anyString(), any(), any()); responder.handleDeliveryInternal("consumerTag", env, props, "ABC".getBytes()).get(1, TimeUnit.SECONDS); verify(channel).basicPublish(anyString(), anyString(), any(), any()); verifyNoMoreInteractions(channel); assertEquals(2, invocations.size()); } @Test public void handleDelivery_forRoutingKey_success() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); ReqEvaluator evaluator = new ReqEvaluator(); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); responder.put(Req.class, evaluator); Req req = new Req(); req.value = 234; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); byte[] reqdata = responder.serializer.encode(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, reqdata); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Res res = responder.serializer.decode(resdata, Res.class); assertEquals(req.value * 2, res.value); assertSame(headers, evaluator.headers); assertEquals(req.value, evaluator.request.value); } @Test public void handleDelivery_forRoutingKey_onMissingEvaluator_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); Req req = new Req(); req.value = 234; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); byte[] reqdata = responder.serializer.encode(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, reqdata); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("No request evaluator found for request class io.ventu.rpc.amqp.AmqpResponderImplTest$Req", res.get("error")); } @Test public void handleDelivery_forRoutingKey_onWrongEvaluatorClass_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); Req req = new Req(); req.value = 234; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); byte[] reqdata = responder.serializer.encode(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, reqdata); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("No request evaluator found for request class io.ventu.rpc.amqp.AmqpResponderImplTest$Req", res.get("error")); } @Test public void handleDelivery_forRoutingKey_onRequestDecodingException_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); responder.put(Req.class, new ReqEvaluator()); Req req = new Req(); req.value = 234; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, new byte[] {}); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("failed to decode JSON", res.get("error")); } @Test public void handleDelivery_forRoutingKey_onResponseEvaluationException_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); responder.put(Req.class, new ReqEvaluator()); Req req = new Req(); req.value = 150; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); byte[] reqdata = responder.serializer.encode(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, reqdata); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("boom", res.get("error")); } @Test public void handleDelivery_onDeroutingException_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); responder.put(Req.class, new ReqEvaluator()); String routingKey = "INCORRECT ROUTE"; CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, null, null); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("Failed to de-route 'INCORRECT ROUTE'", res.get("error")); } @Test public void handleDelivery_forRoutingKey_onResponseEncodingException_apiErrorReturned() throws TimeoutException, IOException, EncodingException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; ChannelProvider provider = mock(ChannelProvider.class); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern, new DefaultSerializer() { @Override public byte[] encode(Object value) throws EncodingException { if (value instanceof Req) { return super.encode(value); } throw new EncodingException("failed to encode JSON"); } }, new DefaultRequestRouter()); responder.put(Req.class, new ReqEvaluator()); Req req = new Req(); req.value = 234; Map<String, Object> headers = Maps.newHashMap(); headers.put("apikey", "234435345345"); String routingKey = responder.requestRouter.route(req); byte[] reqdata = responder.serializer.encode(req); CompletableFuture<byte[]> answer = responder.handleDelivery(routingKey, headers, reqdata); byte[] resdata = answer.get(1, TimeUnit.SECONDS); Map<Object, Object> res = responder.serializer.decode(resdata, Map.class); assertEquals(1, res.size()); assertEquals("failed to encode JSON", res.get("error")); } @Test public void close_success() throws IOException, TimeoutException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; Channel channel = mock(Channel.class); ChannelProvider provider = mock(ChannelProvider.class); doReturn(channel).when(provider).provide(anyString(), any()); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); responder.close().get(1, TimeUnit.SECONDS); verify(channel).close(); verifyNoMoreInteractions(channel); } @Test public void close_onIOException_exception() throws IOException, TimeoutException, ExecutionException, InterruptedException { String routingKeyPattern = "routingKeyPattern"; Channel channel = mock(Channel.class); doThrow(new IOException("boom")).when(channel).close(); ChannelProvider provider = mock(ChannelProvider.class); doReturn(channel).when(provider).provide(anyString(), any()); AmqpResponderImpl responder = new AmqpResponderImpl(provider, routingKeyPattern); CompletableFuture<Void> answer = responder.close(); exception.expect(ExecutionException.class); try { answer.get(1, TimeUnit.SECONDS); } catch (ExecutionException ex) { verify(channel).close(); verifyNoMoreInteractions(channel); assertTrue(ex.getCause() instanceof IOException); assertEquals("boom", ex.getCause().getMessage()); throw ex; } } @Test public void channelProvider_provide_channelConstructedCorrectly() throws IOException, TimeoutException { String routingKeyPattern = "io.ventu.#"; String rpcQueue = "rpc"; String rpcExchange = "exchange"; DeclareOk declareOk = mock(DeclareOk.class); doReturn(rpcQueue).when(declareOk).getQueue(); Channel channel = mock(Channel.class); doReturn(declareOk).when(channel).queueDeclare(rpcQueue, false, false, false, Maps.newHashMap()); Connection conn = mock(Connection.class); doReturn(channel).when(conn).createChannel(); ConnectionFactory factory = mock(ConnectionFactory.class); doReturn(conn).when(factory).newConnection(); ChannelProvider provider = new ChannelProviderImpl(factory, rpcExchange, rpcQueue); Consumer consumer = mock(Consumer.class); Channel actual = provider.provide(routingKeyPattern, consumer); assertSame(channel, actual); verify(channel).exchangeDeclare(rpcExchange, "topic"); verify(channel).queueDeclare(rpcQueue, false, false, false, Maps.newHashMap()); verify(channel).queueBind(rpcQueue, rpcExchange, routingKeyPattern); verify(channel).basicConsume(rpcQueue, true, consumer); verifyNoMoreInteractions(channel); } }