package com.workshare.msnos.core.protocols.ip.www; import static com.workshare.msnos.core.CoreHelper.synchronousGatewayMulticaster; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.ParseException; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import com.workshare.msnos.core.Cloud; import com.workshare.msnos.core.Gateway; import com.workshare.msnos.core.Gateway.Listener; import com.workshare.msnos.core.Iden; import com.workshare.msnos.core.Message; import com.workshare.msnos.core.Message.Status; import com.workshare.msnos.core.MessageBuilder; import com.workshare.msnos.core.Receipt; import com.workshare.msnos.core.serializers.WireJsonSerializer; import com.workshare.msnos.core.serializers.WireSerializer; public class WWWGatewayTest { private static final String WWW_ROOT = "https://www.wombats.com/"; private static final UUID uuid1 = new UUID(111, 111); private static final UUID uuid2 = new UUID(222, 222); private static final UUID uuid3 = new UUID(333, 333); private static final UUID CLOUD_UUID = UUID.randomUUID(); private WWWGateway gate; private WireSerializer serializer; private ScheduledExecutorService scheduler; private List<Message> rxMessages; private Cloud cloud; private HttpClientHelper http; private WWWSynchronizer synchro; private WWWSynchronizer.Processor processor; @Before public void setup() throws Exception { cloud = new Cloud(CLOUD_UUID, " ", Collections.<Gateway>emptySet()); System.setProperty(WWWGateway.SYSP_ADDRESS, WWW_ROOT); http = new HttpClientHelper(); scheduler = mock(ScheduledExecutorService.class); serializer = mockWireSerializer(); rxMessages = new ArrayList<Message>(); synchro = mock(WWWSynchronizer.class); processor = mock(WWWSynchronizer.Processor.class); when(synchro.init(any(Cloud.class))).thenReturn(processor); gate = new WWWGateway(client(), synchro , scheduler, serializer, synchronousGatewayMulticaster()); gate.addListener(cloud, new Listener() { @Override public void onMessage(Message message) { rxMessages.add(message); } }); } @Test public void shouldNotSendMessagesStraightAway() throws Exception { gate.send(cloud, message(uuid1), null); assertNull(http.getLastPostToWWW()); } @Test public void shouldReturnAReceiptOnSend() throws Exception { Receipt receipt = gate.send(cloud, message(uuid1), null); assertNotNull(receipt); assertEquals(uuid1, receipt.getMessageUuid()); assertEquals(Status.PENDING, receipt.getStatus()); assertEquals("WWW", receipt.getGate()); } @Test public void shouldSendNothingWhenNoMessageAndSync() throws Exception { scheduledTask().run(); assertNull(http.getLastPostToWWW()); } @Test public void shouldSendOneMessageOnSync() throws Exception { gate.send(cloud, message(uuid1), null); scheduledTask().run(); HttpPost request = http.getLastPostToWWW(); assertEquals(messagesRequestUrl(cloud), request.getURI().toString()); assertEquals(toText(uuid1), toText(request.getEntity())); } @Test public void shouldSendMultipleMessageOnSync() throws Exception { gate.send(cloud, message(uuid1), null); gate.send(cloud, message(uuid2), null); gate.send(cloud, message(uuid3), null); scheduledTask().run(); HttpPost request = http.getLastPostToWWW(); String expected = toText(uuid1) + toText(uuid2) + toText(uuid3); String current = toText(request.getEntity()); assertEquals(expected, current); } @Test public void shouldExecuteTwoDifferentHttpCallsWhenSendingMessagesForTwoClouds() throws Exception { Cloud otherCloud = mock(Cloud.class); when(otherCloud.getIden()).thenReturn(new Iden(Iden.Type.CLD, UUID.randomUUID())); gate.send(cloud, message(uuid1), null); gate.send(otherCloud, message(uuid2), null); scheduledTask().run(); List<HttpPost> requests = http.getAllRequestToWWW(HttpPost.class); assertEquals(2, requests.size()); http.assertRequestsContains(requests, messagesRequestUrl(cloud)); http.assertRequestsContains(requests, messagesRequestUrl(otherCloud)); } @Test public void shouldInvokeGetMessagesOnSync() throws Exception { scheduledTask().run(); HttpGet request = http.getLastGetToWWW(); assertEquals(messagesRequestUrl(cloud), request.getURI().toString()); } @Test public void shouldInvokeGetMessagesOnSyncStartingFromTheLastOne() throws Exception { final Message message = new MessageBuilder(Message.Type.PIN, cloud, cloud).make(); mockGetResponse(message); scheduledTask().run(); scheduledTask().run(); HttpGet request = http.getLastGetToWWW(); assertEquals(messagesRequestUrl(cloud, message), request.getURI().toString()); } @Test public void shouldInvokeProcessorOnReceivedMessagesTheVeryFirstTime() throws Exception { final Message ping = new MessageBuilder(Message.Type.PIN, cloud, cloud).make(); final Message pong = new MessageBuilder(Message.Type.PON, cloud, cloud).make(); mockGetResponse(ping, pong); scheduledTask().run(); assertEquals(0, rxMessages.size()); verify(synchro).init(cloud); verify(processor).accept(ping); verify(processor).accept(pong); verify(processor).commit(); } @Test public void shouldInvokeListenerOnReceivedMessagesTheSecondTime() throws Exception { scheduledTask().run(); Mockito.reset(synchro); mockGetResponse(new MessageBuilder(Message.Type.PIN, cloud, cloud).make()); scheduledTask().run(); assertEquals(1, rxMessages.size()); verifyZeroInteractions(synchro); } @Test public void shouldAvoidConcurrentSync() throws Exception { final AtomicInteger runs = new AtomicInteger(0); when(client().execute(any(HttpUriRequest.class))).thenAnswer(new Answer<HttpResponse>() { @Override public HttpResponse answer(InvocationOnMock invocation) throws Throwable { runs.incrementAndGet(); sleep(250l); return response(); } }); waitFor(asyncRun(scheduledTask()), asyncRun(scheduledTask()), asyncRun(scheduledTask())); assertEquals(1, runs.get()); } @Test(expected = IOException.class) public void shouldBlowUpIfCannotContactTheServer() throws Exception { mockExceptionResponse(); gate = new WWWGateway(client(), scheduler, serializer, synchronousGatewayMulticaster()); } @Test public void shouldInvokeGetMessagesOnSyncRestartingAfterConsecutiveErrors() throws Exception { final Message message = new MessageBuilder(Message.Type.PIN, cloud, cloud).make(); mockGetResponse(message); scheduledTask().run(); mockExceptionResponse(); for (int i=0; i<WWWGateway.MAX_TOTAL_CONSECUTIVE_ERRORS; i++) scheduledTask().run(); scheduledTask().run(); HttpGet request = http.getLastGetToWWW(); assertEquals(messagesRequestUrl(cloud), request.getURI().toString()); } @Test public void shouldInvokeGetMessagesOnSyncStartingFromTheLastOneIfNotEnoughConsecutiveErrors() throws Exception { final Message message = new MessageBuilder(Message.Type.PIN, cloud, cloud).make(); for (int i=0; i<WWWGateway.MAX_TOTAL_CONSECUTIVE_ERRORS; i++) { http.reset(); mockExceptionResponse(); scheduledTask().run(); http.reset(); mockGetResponse(message); scheduledTask().run(); } HttpGet request = http.getLastGetToWWW(); assertEquals(messagesRequestUrl(cloud, message), request.getURI().toString()); } private void mockExceptionResponse() throws IOException, ClientProtocolException { when(client().execute(any(HttpUriRequest.class))).thenThrow(new IOException("boom!")); } private void mockGetResponse(Message... messages) throws UnsupportedEncodingException { StringBuilder input = new StringBuilder(); for (Message message : messages) { input.append(toWireJson(message)); input.append("\n"); } when(response().getEntity()).thenReturn(new StringEntity(input.toString())); } private void waitFor(Thread... threads) throws InterruptedException { for (Thread thread : threads) { thread.join(); } } private Thread asyncRun(final Runnable task) { final Thread t = new Thread(task); t.start(); return t; } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.interrupted(); } } private Runnable scheduledTask() { ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler).scheduleAtFixedRate(captor.capture(), anyLong(), anyLong(), any(TimeUnit.class)); return captor.getValue(); } private Message message(UUID uuid) { final Message msg = mock(Message.class); when(msg.getUuid()).thenReturn(uuid); return msg; } private String toText(final UUID uuid) { return uuid.toString() + "\n"; } public String toText(HttpEntity entity) throws ParseException, IOException { return EntityUtils.toString(entity); } private WireSerializer mockWireSerializer() { WireSerializer serializer = mock(WireSerializer.class); when(serializer.toText(anyObject())).thenAnswer(new Answer<String>() { @Override public String answer(InvocationOnMock invocation) throws Throwable { Message msg = (Message) invocation.getArguments()[0]; final UUID uuid = msg.getUuid(); return uuid.toString(); } }); Answer<Message> answer = new Answer<Message>() { @Override public Message answer(InvocationOnMock invocation) throws Throwable { String text = (String) invocation.getArguments()[0]; return fromWireJson(text); } }; when(serializer.fromText(anyString(), (Class<?>) any())).thenAnswer(answer); return serializer; } private String toWireJson(Message message) { return new WireJsonSerializer().toText(message); } private Message fromWireJson(String text) { return new WireJsonSerializer().fromText(text, Message.class); } private String messagesRequestUrl(final Cloud cloud) { return messagesRequestUrl(cloud, null); } private String messagesRequestUrl(final Cloud cloud, final Message message) { String url = WWW_ROOT + "api/1.0/messages?cloud=" + cloud.getIden().getUUID(); if (message != null) url = url + "&message=" + message.getUuid().toString(); return url; } private HttpClient client() { return http.client(); } private HttpResponse response() { return http.response(); } }