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.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.rabbitmq.client.AMQP.BasicProperties; 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.RemoteInvoker; import io.ventu.rpc.amqp.AmqpInvokerImpl.ChannelProvider; import io.ventu.rpc.amqp.AmqpInvokerImpl.ChannelProviderImpl; import io.ventu.rpc.amqp.AmqpInvokerImpl.ResponseReceiver; import io.ventu.rpc.amqp.AmqpInvokerImpl.ResponseReceiverImpl; import io.ventu.rpc.amqp.defaults.DefaultRequestRouter; import io.ventu.rpc.amqp.defaults.DefaultSerializer; import io.ventu.rpc.amqp.defaults.UidGenerator; import io.ventu.rpc.exception.ApiException; import io.ventu.rpc.exception.EncodingException; import io.ventu.rpc.util.Serializer; import io.ventu.rpc.util.Validator; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import static io.ventu.rpc.RemoteInvoker.CONTENT_TYPE; import static io.ventu.rpc.RemoteInvoker.ENCODING; import static io.ventu.rpc.amqp.AmqpInvoker.DEFAULT_RPC_EXCHANGE; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; 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 AmqpInvokerimplTest { @Rule public ExpectedException exception = ExpectedException.none(); static Serializer serializer = new DefaultSerializer(); public static class Req { public int value; } public static class Res { public int value; } @Test public void constructor_channelProviderSuppliesChannel() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Channel channel = mock(Channel.class); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); new AmqpInvokerImpl(instanceId, channelProvider, receiver); verify(channelProvider).provide(instanceId, receiver); verifyNoMoreInteractions(channelProvider); } @Test public void invoke_onOkRequest_encodedAndCorrectlyPublishedToAMQP() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Req req = new Req(); Channel channel = mock(Channel.class); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); doReturn(answer).when(receiver).put(anyString(), any()); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); doReturn(DEFAULT_RPC_EXCHANGE).when(channelProvider).rpcExchange(); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver); CompletableFuture<Res> actual = invoker.invoke(req, Res.class); assertSame(answer, actual); assertFalse(actual.isDone()); assertFalse(actual.isCompletedExceptionally()); verify(channelProvider).provide(instanceId, receiver); verify(channelProvider).rpcExchange(); verifyNoMoreInteractions(channelProvider); verify(channel).basicPublish(anyString(), any(), any(), any()); verifyNoMoreInteractions(channel); verify(receiver).put(anyString(), any()); verifyNoMoreInteractions(receiver); } @Test public void invoke_onOkRequest_correctDataPassedToPublisher() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Req req = new Req(); Map<String, Object> headers = Maps.newHashMap(); headers.put("key", Integer.valueOf(12341234)); Channel channel = mock(Channel.class); doAnswer(invocation -> { assertEquals(DEFAULT_RPC_EXCHANGE, invocation.getArguments()[0]); assertEquals(Req.class.getName(), invocation.getArguments()[1]); BasicProperties props = (BasicProperties) invocation.getArguments()[2]; assertEquals(instanceId, props.getAppId()); assertEquals(instanceId, props.getReplyTo()); assertEquals(CONTENT_TYPE, props.getContentType()); assertEquals(headers.get("key"), props.getHeaders().get("key")); assertEquals(ENCODING, props.getContentEncoding()); String actual = new String((byte[]) invocation.getArguments()[3]); assertEquals(new String(serializer.encode(req)), actual); return null; }).when(channel).basicPublish(anyString(), any(), any(), any()); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); doReturn(answer).when(receiver).put(anyString(), any()); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); doReturn(DEFAULT_RPC_EXCHANGE).when(channelProvider).rpcExchange(); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver, new DefaultRequestRouter(), new UidGenerator() { }, serializer, headers); invoker.invoke(req, Res.class); // make sure it was invoked, otherwise our assertions will be void verify(channel).basicPublish(anyString(), any(), any(), any()); } @Test public void invoke_onOkRequest_onIOException_futureCompletesExceptionally() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Req req = new Req(); Channel channel = mock(Channel.class); doAnswer(invocation -> { throw new IOException("boom"); }).when(channel).basicPublish(anyString(), any(), any(), any()); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); doReturn(answer).when(receiver).put(anyString(), any()); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); doReturn(DEFAULT_RPC_EXCHANGE).when(channelProvider).rpcExchange(); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver); CompletableFuture<Res> actual = invoker.invoke(req, Res.class); assertSame(answer, actual); assertTrue(actual.isDone()); assertTrue(actual.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { actual.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof IOException); throw ex; } } @Test public void invoke_onNokRequest_onIllegalArgEx_futureCompletesExceptionally() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Req req = new Req(); Channel channel = mock(Channel.class); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); doReturn(answer).when(receiver).put(anyString(), any()); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); ObjectMapper mapper = mock(ObjectMapper.class); doThrow(IllegalArgumentException.class).when(mapper).writeValueAsBytes(any()); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver, new DefaultRequestRouter(), new UidGenerator() { }, new DefaultSerializer(mapper), Maps.newHashMap()); CompletableFuture<Res> actual = invoker.invoke(req, Res.class); assertSame(answer, actual); assertTrue(actual.isDone()); assertTrue(actual.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { actual.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof IllegalArgumentException); throw ex; } } @Test public void invoke_onNokRequest_onEncodingEx_futureCompletesExceptionally() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Req req = new Req(); Channel channel = mock(Channel.class); CompletableFuture<Res> answer = new CompletableFuture<>(); ResponseReceiver receiver = mock(ResponseReceiver.class); doReturn(answer).when(receiver).put(anyString(), any()); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); ObjectMapper mapper = mock(ObjectMapper.class); doThrow(JsonProcessingException.class).when(mapper).writeValueAsBytes(any()); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver, new DefaultRequestRouter(), new UidGenerator() { }, new DefaultSerializer(mapper), Maps.newHashMap()); CompletableFuture<Res> actual = invoker.invoke(req, Res.class); assertSame(answer, actual); assertTrue(actual.isDone()); assertTrue(actual.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { actual.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof EncodingException); throw ex; } } @Test public void closingInvoker_success() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Channel channel = mock(Channel.class); ResponseReceiver receiver = mock(ResponseReceiver.class); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver); CompletableFuture<Void> actual = invoker.close(); verify(channel).close(); verifyNoMoreInteractions(channel); actual.get(); assertTrue(actual.isDone()); assertFalse(actual.isCompletedExceptionally()); } @Test public void closingInvoker_onIOException_exception() throws IOException, TimeoutException, ExecutionException, InterruptedException { String instanceId = "123456789"; Channel channel = mock(Channel.class); doThrow(new IOException("boom")).when(channel).close(); ResponseReceiver receiver = mock(ResponseReceiver.class); ChannelProvider channelProvider = mock(ChannelProvider.class); doReturn(channel).when(channelProvider).provide(instanceId, receiver); RemoteInvoker invoker = new AmqpInvokerImpl(instanceId, channelProvider, receiver); CompletableFuture<Void> actual = invoker.close(); verify(channel).close(); verifyNoMoreInteractions(channel); assertTrue(actual.isDone()); assertTrue(actual.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { actual.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof IOException); throw ex; } } @Test public void channelProvider_provide_channelConstructedCorrectly_withNoQueueOriginallySet() throws IOException, TimeoutException { String instanceId = "123456789"; String responseQueue = "queue"; String rpcExchange = "exchange"; DeclareOk declareOk = mock(DeclareOk.class); doReturn(responseQueue).when(declareOk).getQueue(); Channel channel = mock(Channel.class); doReturn(declareOk).when(channel).queueDeclare("", 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, ""); Consumer consumer = mock(Consumer.class); Channel actual = provider.provide(instanceId, consumer); assertSame(channel, actual); verify(channel).exchangeDeclare(rpcExchange, "topic"); verify(channel).queueDeclare("", false, false, false, Maps.newHashMap()); verify(channel).queueBind(responseQueue, rpcExchange, instanceId); verify(channel).basicConsume(responseQueue, true, consumer); verifyNoMoreInteractions(channel); } @Test public void channelProvider_provide_channelConstructedCorrectly_withGivenQueue() throws IOException, TimeoutException { String instanceId = "123456789"; String responseQueue = "queue"; String rpcExchange = "exchange"; DeclareOk declareOk = mock(DeclareOk.class); doReturn(responseQueue).when(declareOk).getQueue(); Channel channel = mock(Channel.class); doReturn(declareOk).when(channel).queueDeclare(responseQueue, 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, responseQueue); Consumer consumer = mock(Consumer.class); Channel actual = provider.provide(instanceId, consumer); assertSame(channel, actual); verify(channel).exchangeDeclare(rpcExchange, "topic"); verify(channel).queueDeclare(responseQueue, false, false, false, Maps.newHashMap()); verify(channel).queueBind(responseQueue, rpcExchange, instanceId); verify(channel).basicConsume(responseQueue, true, consumer); verifyNoMoreInteractions(channel); } @Test public void responseReceiver_put_createsFuture_andCleansUpExpires() throws InterruptedException, ExecutionException, TimeoutException { ResponseReceiver receiver = new ResponseReceiverImpl(serializer, new Validator() { }, 100, TimeUnit.MILLISECONDS); CompletableFuture<Res> actual = receiver.put("987654321", Res.class); assertFalse(actual.isDone()); assertFalse(actual.isCompletedExceptionally()); // wait for expiry timeout Thread.sleep(150); // trigger cache operations (to evict the record) for (int i = 0; i < 1000; i++) { receiver.put(UUID.randomUUID().toString(), Res.class); } exception.expect(ExecutionException.class); try { actual.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof TimeoutException); assertEquals( "Request io.ventu.rpc.amqp.AmqpInvokerimplTest$Res with correlationId 987654321 has expired.", ex.getCause().getMessage()); throw ex; } } @Test public void responseReceiver_handleDelivery_dispatchesToLocalOnCorrelationId() throws IOException { final String correlationId = "987654321"; final String payload = "payload"; final List<Boolean> invocations = Lists.newArrayList(); ResponseReceiver receiver = new ResponseReceiverImpl(serializer, new Validator() { }, 1, TimeUnit.MINUTES) { @Override void handleDelivery(String actualCorrelationId, byte[] actualBody) throws IOException { invocations.add(Boolean.TRUE); assertEquals(correlationId, actualCorrelationId); assertEquals(payload, new String(actualBody)); } }; Envelope env = new Envelope(1L, false, "exchange", "routingKey"); BasicProperties props = new BasicProperties.Builder().correlationId(correlationId).build(); receiver.handleDelivery(null, null, props, payload.getBytes()); assertEquals(1, invocations.size()); } @Test public void responseReceiver_handleDelivery_responseValidated() throws EncodingException, IOException, InterruptedException, ExecutionException, TimeoutException { final List<Boolean> invocations = Lists.newArrayList(); Validator validator = new Validator() { @Override public <T> void validate(T value) throws ApiException, IllegalArgumentException { invocations.add(Boolean.TRUE); assertTrue(value instanceof Res); assertEquals(25, ((Res) value).value); } }; ResponseReceiverImpl receiver = new ResponseReceiverImpl(serializer, validator, 1, TimeUnit.MINUTES); Res res = new Res(); res.value = 25; String correlationId = "987654321"; CompletableFuture<Res> answer = receiver.put(correlationId, Res.class); assertFalse(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); receiver.handleDelivery(correlationId, serializer.encode(res)); assertEquals(1, invocations.size()); assertTrue(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); Res actual = answer.get(500, TimeUnit.MILLISECONDS); assertEquals(25, actual.value); } @Test public void responseReceiver_handleDelivery_onEncodingException_exception() throws EncodingException, IOException, InterruptedException, ExecutionException, TimeoutException { ResponseReceiverImpl receiver = new ResponseReceiverImpl(serializer, new Validator() { }, 1, TimeUnit.MINUTES); String correlationId = "987654321"; CompletableFuture<Res> answer = receiver.put(correlationId, Res.class); assertFalse(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); receiver.handleDelivery(correlationId, new byte[] {}); assertTrue(answer.isDone()); assertTrue(answer.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { answer.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof EncodingException); assertEquals("failed to decode JSON", ex.getCause().getMessage()); throw ex; } } @Test public void responseReceiver_handleDelivery_onEncodingException_withErrorField_APIException() throws EncodingException, IOException, InterruptedException, ExecutionException, TimeoutException { ResponseReceiverImpl receiver = new ResponseReceiverImpl(serializer, new Validator() { }, 1, TimeUnit.MINUTES); String correlationId = "987654321"; CompletableFuture<Res> answer = receiver.put(correlationId, Res.class); assertFalse(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); Map<String, Object> res = Maps.newHashMap(); res.put("error", Integer.valueOf(371)); receiver.handleDelivery(correlationId, serializer.encode(res)); assertTrue(answer.isDone()); assertTrue(answer.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { answer.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof ApiException); assertEquals("371", ex.getCause().getMessage()); throw ex; } } @Test public void responseReceiver_handleDelivery_onEncodingException_MapWithNoError_exception() throws EncodingException, IOException, InterruptedException, ExecutionException, TimeoutException { ResponseReceiverImpl receiver = new ResponseReceiverImpl(serializer, new Validator() { }, 1, TimeUnit.MINUTES); String correlationId = "987654321"; CompletableFuture<Res> answer = receiver.put(correlationId, Res.class); assertFalse(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); Map<String, Object> res = Maps.newHashMap(); res.put("value", "notAnInt"); receiver.handleDelivery(correlationId, serializer.encode(res)); assertTrue(answer.isDone()); assertTrue(answer.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { answer.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof EncodingException); assertEquals("failed to decode JSON", ex.getCause().getMessage()); throw ex; } } @Test public void responseReceiver_handleDelivery_onAPIException_exception() throws EncodingException, IOException, InterruptedException, ExecutionException { Validator validator = new Validator() { @Override public <T> void validate(T value) throws ApiException, IllegalArgumentException { throw new ApiException("boom"); } }; ResponseReceiverImpl receiver = new ResponseReceiverImpl(serializer, validator, 1, TimeUnit.MINUTES); String correlationId = "987654321"; CompletableFuture<Res> answer = receiver.put(correlationId, Res.class); assertFalse(answer.isDone()); assertFalse(answer.isCompletedExceptionally()); receiver.handleDelivery(correlationId, serializer.encode(Maps.newHashMap())); assertTrue(answer.isDone()); assertTrue(answer.isCompletedExceptionally()); exception.expect(ExecutionException.class); try { answer.get(); } catch (ExecutionException ex) { assertTrue(ex.getCause() instanceof ApiException); assertEquals("boom", ex.getCause().getMessage()); throw ex; } } }