io.ventu.rpc.amqp.AmqpInvokerimplTest.java Source code

Java tutorial

Introduction

Here is the source code for io.ventu.rpc.amqp.AmqpInvokerimplTest.java

Source

/*
 * 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;
        }
    }
}