package com.workshare.msnos.core; import static com.workshare.msnos.core.CoreHelper.asNetwork; import static com.workshare.msnos.core.CoreHelper.asPublicNetwork; import static com.workshare.msnos.core.CoreHelper.asSet; import static com.workshare.msnos.core.CoreHelper.fakeElapseTime; import static com.workshare.msnos.core.CoreHelper.fakeSystemTime; import static com.workshare.msnos.core.CoreHelper.getCloudInternal; import static com.workshare.msnos.core.CoreHelper.randomUUID; import static com.workshare.msnos.core.CoreHelper.synchronousCloudMulticaster; import static com.workshare.msnos.core.Message.Type.APP; import static com.workshare.msnos.core.Message.Type.FLT; import static com.workshare.msnos.core.Message.Type.PIN; import static com.workshare.msnos.core.Message.Type.PRS; import static com.workshare.msnos.core.MessagesHelper.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import com.workshare.msnos.core.Cloud.Internal; import com.workshare.msnos.core.Message.Status; import com.workshare.msnos.core.Message.Type; import com.workshare.msnos.core.cloud.Multicaster; import com.workshare.msnos.core.payloads.FltPayload; import com.workshare.msnos.core.payloads.Presence; import com.workshare.msnos.core.protocols.ip.BaseEndpoint; import com.workshare.msnos.core.protocols.ip.Endpoint; import com.workshare.msnos.core.protocols.ip.Endpoints; import com.workshare.msnos.core.protocols.ip.HttpEndpoint; import com.workshare.msnos.core.protocols.ip.http.HttpGateway; import com.workshare.msnos.core.receipts.SingleReceipt; import com.workshare.msnos.core.security.KeysStore; import com.workshare.msnos.core.security.Signer; import com.workshare.msnos.soup.json.Json; import com.workshare.msnos.soup.time.NTPClient; import com.workshare.msnos.soup.time.SystemTime; public class CloudTest { private static final String KEY_ID = "123"; private static final String KEY_VAL = "ABC"; private static final Iden SOMEONE = new Iden(Iden.Type.AGT, UUID.randomUUID()); private static final Iden SOMEONELSE = new Iden(Iden.Type.AGT, UUID.randomUUID()); private static final Iden MY_CLOUD = new Iden(Iden.Type.CLD, UUID.randomUUID()); private HttpGateway httpGate; private Set<Gateway> gates; private Cloud thisCloud; private Cloud otherCloud; private Sender sender; private ScheduledExecutorService scheduler; private List<Message> receivedMessages; private KeysStore keystore; private Signer signer; private File home; private Receiver receiver; private Multicaster caster; @Before public void init() throws Exception { home = File.createTempFile("msnos-", ".tmp"); System.setProperty("user.home", home.toString()); home.delete(); home.mkdirs(); sender = mock(Sender.class); Receipt receipt = mock(Receipt.class); when(sender.send(any(Cloud.class), any(Message.class))).thenReturn(receipt ); caster = synchronousCloudMulticaster(); scheduler = mock(ScheduledExecutorService.class); Receipt unknownReceipt = mock(Receipt.class); when(unknownReceipt.getStatus()).thenReturn(Status.UNKNOWN); httpGate = mock(HttpGateway.class); when(httpGate.name()).thenReturn("HTTP"); Endpoints endpoints = mock(Endpoints.class); when(httpGate.endpoints()).thenReturn(endpoints); when(httpGate.send(any(Cloud.class), any(Message.class), any(Identifiable.class))).thenReturn(unknownReceipt); keystore = mock(KeysStore.class); signer = new Signer(keystore); NTPClient timeClient = mock(NTPClient.class); when(timeClient.getTime()).thenReturn(1234L); gates = new LinkedHashSet<Gateway>(Arrays.asList(httpGate)); thisCloud = new Cloud(MY_CLOUD.getUUID(), KEY_ID, signer, sender, receiver, gates, caster, scheduler); thisCloud.addListener(new Cloud.Listener() { @Override public void onMessage(Message message) { receivedMessages.add(message); } }); receivedMessages = new ArrayList<Message>(); otherCloud = new Cloud(UUID.randomUUID(), KEY_ID, signer, sender, receiver, Gateways.NONE, caster, Executors.newSingleThreadScheduledExecutor()); } @After public void after() throws Exception { SystemTime.reset(); scheduler.shutdown(); home.delete(); } @Test public void shouldCreateDefaultGateways() throws Exception { Set<Gateway> expected = Gateways.all(); Set<Gateway> current = new Cloud(MY_CLOUD.getUUID()).getGateways(); assertEquals(expected, current); } @Test public void shouldSendPresenceMessageWhenAgentJoins() throws Exception { LocalAgent smith = new LocalAgent(UUID.randomUUID()); smith.join(thisCloud); assertMessageSent(Message.Type.PRS, smith.getIden(), thisCloud.getIden(), null); } @Test public void shouldSendDiscoveryMessageWhenAgentJoins() throws Exception { LocalAgent smith = new LocalAgent(UUID.randomUUID()); smith.join(thisCloud); assertMessageSent(Message.Type.DSC, smith.getIden(), thisCloud.getIden(), null); } @Test public void shouldUpdateAgentsListWhenAgentJoins() throws Exception { LocalAgent smith = new LocalAgent(UUID.randomUUID()); smith.join(thisCloud); assertTrue(thisCloud.getLocalAgents().contains(smith)); } @Test public void shouldDoNothingWhenKnownAgentPongs() throws Exception { RemoteAgent remote = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(remote, thisCloud); simulateMessageFromNetwork(newPongMessage(remote, thisCloud)); assertNoMessagesSent(); } @Test public void shouldSendDiscoveryWhenUnknownAgentPongs() throws Exception { RemoteEntity remote = newRemoteAgent(thisCloud); simulateMessageFromNetwork(newPongMessage(remote, thisCloud)); assertMessageSent(Message.Type.DSC, thisCloud.getIden(), remote.getIden(), null); } @Test public void shouldRemoveLocalAgentOnLeave() throws Exception { LocalAgent jeff = new LocalAgent(UUID.randomUUID()); jeff.join(thisCloud); jeff.leave(); assertFalse(thisCloud.getLocalAgents().contains(jeff)); } @Test public void shouldRemoveRemoteAgentOnLeave() throws Exception { RemoteAgent remote = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(remote, thisCloud); simulateAgentLeavingCloud(remote, thisCloud); assertFalse(thisCloud.getLocalAgents().contains(remote)); } @Test public void shouldSendAbsenceWhenLeavingCloudUsingSyncCall() throws Exception { LocalAgent karl = new LocalAgent(UUID.randomUUID()); karl.join(thisCloud); karl.leave(); final Message message = getLastMessageSentSynchronously(); assertMessageContent(message, PRS, karl.getIden(), thisCloud.getIden(), new Presence(false, karl)); } @Test(expected = Throwable.class) public void shouldGetExceptionWhenTryingToLeaveTheCloudWhileNotJoined() throws Exception { LocalAgent karl = new LocalAgent(UUID.randomUUID()); karl.leave(); } @Test public void shouldNOTUpdateAgentsListWhenAgentJoinsThroughGatewayToAnotherCloud() throws Exception { RemoteAgent frank = newRemoteAgent(otherCloud); simulateAgentJoiningCloud(frank, otherCloud); assertFalse(thisCloud.getLocalAgents().contains(frank)); } @Test public void shouldUpdateRemoteAgentAccessTimeOnPresenceReceived() throws Exception { RemoteAgent remoteAgent = newRemoteAgent(thisCloud); fakeSystemTime(12345L); simulateMessageFromNetwork(newMessage(Message.Type.PRS, remoteAgent.getIden(), thisCloud.getIden()).data(new Presence(true, remoteAgent))); fakeElapseTime(100); assertEquals(12345L, getRemoteAgentAccessTime(thisCloud, remoteAgent)); } @Test public void shouldUpdateRemoteAgentAccessTimeOnMessageReceived() throws Exception { RemoteAgent remoteAgent = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(remoteAgent, thisCloud); fakeSystemTime(99911L); final Message message = newPingMessage(remoteAgent, thisCloud); simulateMessageFromNetwork(message); fakeElapseTime(100); assertEquals(99911L, getRemoteAgentAccessTime(thisCloud, remoteAgent)); } @Test public void shouldPingAgentsWhenAccessTimeIsTooOld() throws Exception { fakeSystemTime(12345L); RemoteAgent remoteAgent = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(remoteAgent, thisCloud); fakeSystemTime(9999999999L); forceRunCloudPeriodicCheck(); Message pingExpected = getLastMessageSent(); assertNotNull(pingExpected); assertEquals(PIN, pingExpected.getType()); assertEquals(thisCloud.getIden(), pingExpected.getFrom()); } @Test public void shouldRemoveAgentsThatDoNOTRespondToPing() throws Exception { fakeSystemTime(0L); RemoteAgent remoteAgent = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(remoteAgent, thisCloud); fakeSystemTime(Long.MAX_VALUE); forceRunCloudPeriodicCheck(); assertTrue(!thisCloud.getRemoteAgents().contains(remoteAgent)); } @Test public void shouldSendFaultWhenAgentRemoved() throws Exception { fakeSystemTime(12345L); LocalAgent remoteAgent = new LocalAgent(UUID.randomUUID()); simulateAgentJoiningCloud(remoteAgent, thisCloud); fakeSystemTime(99999999L); forceRunCloudPeriodicCheck(); Message message = getLastMessageSentToCloudListeners(); assertEquals(FLT, message.getType()); assertEquals(thisCloud.getIden(), message.getFrom()); assertEquals(remoteAgent.getIden(), ((FltPayload) message.getData()).getAbout()); } @Test public void shouldStoreHostInfoWhenRemoteAgentJoins() throws Exception { RemoteAgent remoteFrank = newRemoteAgent(thisCloud); Presence presence = (Presence) simulateAgentJoiningCloud(remoteFrank, thisCloud).getData(); RemoteAgent recordedFrank = getRemoteAgent(thisCloud, remoteFrank.getIden()); assertEquals(presence.getEndpoints(), recordedFrank.getEndpoints()); } @Test public void shouldUpdateRemoteAgentsWhenARemoteJoins() throws Exception { RemoteAgent frank = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(frank, thisCloud); assertEquals(frank.getIden(), thisCloud.getRemoteAgents().iterator().next().getIden()); } @Test public void shouldSendSignedMessagesIfKeystoreConfigured() throws Exception { when(keystore.get(KEY_ID)).thenReturn(KEY_VAL); thisCloud.send(newReliableMessage(APP, SOMEONE, SOMEONELSE)); Message message = getLastMessageSent(); assertNotNull(message.getSig()); assertNotNull(message.getRnd()); } @Test public void shouldUpdateHttpGatewayOnRegisterMsnosEndpoints() throws Exception { HttpEndpoint endpoint = mock(HttpEndpoint.class); when(endpoint.getTarget()).thenReturn(new Iden(Iden.Type.AGT, UUID.randomUUID())); thisCloud.registerRemoteMsnosEndpoint(endpoint); verify(httpGate.endpoints()).install(endpoint); } @Test public void shouldUpdateRemoteAgentOnRegisterMsnosEndpoints() throws Exception { RemoteAgent frank = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(frank, thisCloud); HttpEndpoint endpoint = mock(HttpEndpoint.class); when(endpoint.getTarget()).thenReturn(frank.getIden()); thisCloud.registerRemoteMsnosEndpoint(endpoint); RemoteAgent agent = thisCloud.getRemoteAgents().iterator().next(); assertTrue(agent.getEndpoints().contains(endpoint)); } @Test public void shouldUpdateLocalAgentOnRegisterLocalMsnosEndpoints() throws Exception { LocalAgent smith = new LocalAgent(UUID.randomUUID()); smith.join(thisCloud); HttpEndpoint endpoint = mock(HttpEndpoint.class); when(endpoint.getTarget()).thenReturn(smith.getIden()); thisCloud.registerLocalMsnosEndpoint(endpoint); LocalAgent agent = thisCloud.getLocalAgents().iterator().next(); assertTrue(agent.getEndpoints().contains(endpoint)); } // TODO FIXME // public void shouldUpdateHttpGatewayOnUnregisterMsnosEndpoints() throws Exception { // TODO FIXME // public void shouldUpdateRemoteAgentOnUnregisterMsnosEndpoints() throws Exception { // TODO FIXME // public void shouldUpdateLocalAgentOnUnregisterLocalMsnosEndpoints() throws Exception { @Test public void shouldInstallMsnosEndpointWhenRemoteAgentAdded() throws MsnosException { HttpEndpoint endpoint = new HttpEndpoint(asPublicNetwork("25.25.25.25"), "http://foo.com"); RemoteAgent remote = newRemoteAgent(thisCloud, endpoint); Internal internal = getCloudInternal(thisCloud); internal.remoteAgents().add(remote); verify(httpGate.endpoints()).install(endpoint); } @Test public void shouldUninstallMsnosEndpointWhenRemoteAgentRemoved() throws MsnosException { Internal internal = getCloudInternal(thisCloud); HttpEndpoint endpoint = new HttpEndpoint(asPublicNetwork("25.25.25.25"), "http://foo.com"); RemoteAgent remote = newRemoteAgent(thisCloud, endpoint); internal.remoteAgents().add(remote); internal.remoteAgents().remove(remote.getIden()); verify(httpGate.endpoints()).remove(endpoint); } @Test public void shouldProcessExternalMessage() throws MsnosException { RemoteAgent agent = newRemoteAgent(thisCloud); simulateAgentJoiningCloud(agent, thisCloud); Message current = new MessageBuilder(APP, agent, thisCloud).make(); simulateMessageFromNetwork(current); assertEquals(current.getUuid(), getLastMessageSentToCloudListeners().getUuid()); } @Test public void shouldEnquiryUponReceivingMessagesFromUnknownAgents() throws Exception { RemoteAgent smith = newRemoteAgent(thisCloud); simulateMessageFromNetwork(new MessageBuilder(Message.Type.PIN, smith, thisCloud).make()); Message message = getLastMessageSent(); assertEquals(Message.Type.DSC, message.getType()); assertEquals(smith.getIden(), message.getTo()); } @Test public void shouldNOTEnquiryUponReceivingMessagesFromCloud() throws Exception { simulateMessageFromNetwork(new MessageBuilder(Message.Type.PIN, thisCloud, thisCloud).make()); assertEquals(0, getAllMessagesSent().size()); } @Test public void shouldNOTEnquiryUnknownAgentsMultipleTimes() throws Exception { RemoteAgent smith = newRemoteAgent(thisCloud); simulateMessageFromNetwork(new MessageBuilder(Message.Type.PIN, smith, thisCloud).make()); simulateMessageFromNetwork(new MessageBuilder(Message.Type.PIN, smith, thisCloud).make()); List<Message> messageList = getAllMessagesSent(); boolean enquired = false; for (Message message: messageList) { if (Message.Type.DSC == message.getType() && smith.getIden() == message.getTo()) { if (!enquired) enquired = true; else fail("Multiple subsequent enquiries were sent!"); } } } @Test public void shouldSendMessagesTroughSender() throws Exception { Message message = newPingMessage(thisCloud); thisCloud.send(message); verify(sender).send(thisCloud, message); } @Test public void shouldSendMessagesTroughSenderWhenSync() throws Exception { Message message = newPingMessage(thisCloud); thisCloud.sendSync(message); verify(sender).sendSync(eq(thisCloud), eq(message), any(SingleReceipt.class)); } @Test public void shouldAutomaticallyCalculateRing() { final Endpoint htp = new HttpEndpoint(asPublicNetwork("25.25.25.25"), "http://25.25.25.25"); final Endpoint udp = new BaseEndpoint(Endpoint.Type.UDP, asNetwork("192.168.0.199", (short) 16)); final Set<Endpoint> endpoints = asSet(htp, udp); final Gateway gate = mock(Gateway.class); when(gate.endpoints()).thenReturn(CoreHelper.makeImmutableEndpoints(endpoints)); Cloud cloud = new Cloud(randomUUID(), KEY_ID, signer, sender, null, asSet(gate), caster, scheduler); assertEquals(Ring.make(endpoints), cloud.getRing()); } @Test public void shouldAssingCloudRingToLocalAgentOnJoin() throws Exception { LocalAgent smith = new LocalAgent(UUID.randomUUID()); smith.join(thisCloud); assertEquals(thisCloud.getRing(), smith.getRing()); } @Test public void shouldAssingComputedRingToRemoteAgent() throws Exception { RemoteAgent frank = newRemoteAgent(thisCloud); Message message = new MessageBuilder(Message.Type.PRS, frank, thisCloud).with(new Presence(true, frank)).make(); simulateMessageFromNetwork(message); frank = getRemoteAgent(thisCloud, frank.getIden()); assertNotEquals(thisCloud.getRing(), frank.getRing()); } private RemoteAgent getRemoteAgent(Cloud thisCloud, Iden iden) { for (RemoteAgent agent : thisCloud.getRemoteAgents()) { if (agent.getIden().equals(iden)) return agent; } return null; } private void forceRunCloudPeriodicCheck() { Runnable runnable = capturePeriodicRunableCheck(); runnable.run(); } private Runnable capturePeriodicRunableCheck() { ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); verify(scheduler, atLeastOnce()).scheduleAtFixedRate(captor.capture(), anyInt(), anyInt(), any(TimeUnit.class)); return captor.getValue(); } private long getRemoteAgentAccessTime(Cloud cloud, RemoteEntity agent) { Collection<RemoteAgent> agents = cloud.getRemoteAgents(); for (RemoteEntity a : agents) { if (a.getIden().equals(agent.getIden())) return a.getAccessTime(); } throw new RuntimeException("Agent " + agent + " not found!"); } private Message simulateAgentJoiningCloud(Agent agent, Cloud cloud) throws MsnosException { Message message = new MessageBuilder(Message.Type.PRS, agent, cloud).with(new Presence(true, agent)).make(); simulateMessageFromNetwork(message); return message; } private void simulateAgentLeavingCloud(RemoteAgent agent, Cloud cloud) throws MsnosException { simulateMessageFromNetwork(new MessageBuilder(PRS, agent.getIden(), cloud.getIden()).reliable(false).with(new Presence(false, agent)).make()); } private void simulateMessageFromNetwork(final Message message) { ArgumentCaptor<Gateway.Listener> gateListener = ArgumentCaptor.forClass(Gateway.Listener.class); verify(httpGate).addListener(any(Cloud.class), gateListener.capture()); gateListener.getValue().onMessage(message); } private Message getLastMessageSentToCloudListeners() { final int size = receivedMessages.size(); if (size > 0) return receivedMessages.get(size - 1).fromGate(null); else return null; } private List<Message> getAllMessagesSent() throws IOException { try { ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class); verify(sender, atLeastOnce()).send(any(Cloud.class), captor.capture()); return captor.getAllValues(); } catch (Throwable any) { return Collections.emptyList(); } } private Message getLastMessageSent() throws IOException { List<Message> messageList = getAllMessagesSent(); return messageList.get(messageList.size() - 1); } private Message getLastMessageSentSynchronously() throws IOException { try { ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class); verify(sender, atLeastOnce()).sendSync(any(Cloud.class), captor.capture(), any(SingleReceipt.class)); final List<Message> allValues = captor.getAllValues(); return allValues.get(allValues.size()-1); } catch (Throwable any) { return mock(Message.class); } } private Message newMessage(final Message.Type type, final Iden idenFrom, final Iden idenTo) { return new MessageBuilder(type, idenFrom, idenTo).withHops(1).reliable(false).make(); } private Message newReliableMessage(final Message.Type type, final Iden idenFrom, final Iden idenTo) { return new MessageBuilder(type, idenFrom, idenTo).withHops(1).reliable(true).make(); } private void assertNoMessagesSent() throws IOException { assertEquals(0, getAllMessagesSent().size()); } private void assertMessageSent(final Type type, final Iden from, final Iden to, Object data) throws IOException { List<Message> messageList = getAllMessagesSent(); for (Message message : messageList) { if (message.getType() == type) assertMessageContent(message, type, from, to, data); } } private void assertMessageContent(Message message, final Type type, final Iden from, final Iden to, Object data) { assertNotNull(message); assertEquals(type, message.getType()); assertEquals(from, message.getFrom()); assertEquals(to, message.getTo()); if (data != null) assertEquals(Json.toJsonString(data), Json.toJsonString(message.getData())); } private RemoteAgent newRemoteAgent(Cloud cloud, final Endpoint... endpoints) { Set<Endpoint> endpointset = new HashSet<Endpoint>(Arrays.asList(endpoints)); return new RemoteAgent(UUID.randomUUID(), cloud, endpointset ); } }