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

Java tutorial

Introduction

Here is the source code for io.ventu.rpc.amqp.AmqpResponderImplTest.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.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);
    }

}