package com.workshare.msnos.core.protocols.ip.udp; import static com.workshare.msnos.core.CoreHelper.fakeSystemTime; import static com.workshare.msnos.core.CoreHelper.synchronousGatewayMulticaster; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; 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.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.MulticastSocket; import java.net.SocketException; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import com.workshare.msnos.core.Cloud; import com.workshare.msnos.core.Cloud.Internal; 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.protocols.ip.MulticastSocketFactory; import com.workshare.msnos.core.serializers.WireJsonSerializer; public class UDPGatewayTest { private static final Iden ME = new Iden(Iden.Type.AGT, new UUID(123, 999)); private static final Iden SOMEONE = new Iden(Iden.Type.AGT, UUID.randomUUID()); private UDPGateway gate; private UDPServer server; private MulticastSocket socket; private MulticastSocketFactory sockets; private List<Message> messages; private Cloud cloud; @Before public void setup() throws Exception { messages = new ArrayList<Message>(); server = mock(UDPServer.class); when(server.serializer()).thenReturn(new WireJsonSerializer()); socket = mock(MulticastSocket.class); sockets = mock(MulticastSocketFactory.class); when(sockets.create()).thenReturn(socket); cloud = mock(Cloud.class); when(cloud.getIden()).thenReturn(new Iden(Iden.Type.CLD, UUID.randomUUID())); } @Test public void shouldOpenTheSocket() throws Exception { System.setProperty(UDPGateway.SYSP_PORT_NUM, "2727"); gate(); verify(socket).setReuseAddress(true); verify(socket).bind(new InetSocketAddress(2727)); } @Test public void shouldJoinTheUDPGroup() throws Exception { System.setProperty(UDPGateway.SYSP_UDP_GROUP, "230.31.32.33"); ArgumentCaptor<InetAddress> captor = ArgumentCaptor.forClass(InetAddress.class); gate(); verify(socket).joinGroup(captor.capture()); assertEquals("230.31.32.33", captor.getValue().getHostAddress()); } @Test public void shouldUseNextSocketPort() throws Exception { System.setProperty(UDPGateway.SYSP_PORT_NUM, "2727"); System.setProperty(UDPGateway.SYSP_PORT_WIDTH, "2"); doThrow(new SocketException("boom!")).when(socket).bind(new InetSocketAddress(2727)); gate(); verify(socket).bind(new InetSocketAddress(2728)); } @Test(expected = IOException.class) public void shouldBlowUpIfNoPortAvailable() throws Exception { System.setProperty(UDPGateway.SYSP_PORT_NUM, "2727"); System.setProperty(UDPGateway.SYSP_PORT_WIDTH, "1"); doThrow(new SocketException("boom!")).when(socket).bind(new InetSocketAddress(2727)); gate(); } @Test public void shouldSendAMessageTroughTheSocket() throws Exception { Message message = UDPGatewayTest.newSampleMessage(); gate().send(cloud, message, null); List<DatagramPacket> packets = getSentPackets(); assertPacketValid(message, packets.get(0)); } @Test public void shouldResendMessageOnUDPFailure() throws Exception { System.setProperty(UDPGateway.SYSP_RETRY_TIMES, "5"); System.setProperty(UDPGateway.SYSP_PORT_WIDTH, "1"); doThrow(new SocketException("boom!")).when(socket).send(any(DatagramPacket.class)); sendMessageAndIgnoreExceptions(newSampleMessage()); verify(socket, times(5)).send(any(DatagramPacket.class)); } @Test public void shouldResendMessageUsingTransmitPacing() throws Exception { System.setProperty(UDPGateway.SYSP_RETRY_TIMES, "5"); System.setProperty(UDPGateway.SYSP_PORT_WIDTH, "1"); doThrow(new SocketException("boom!")).when(socket).send(any(DatagramPacket.class)); AtomicLong counter = fakeSystemTime(); sendMessageAndIgnoreExceptions(newSampleMessage()); assertEquals(1+3+5+5, counter.get()); } @Test public void shouldReturnAReceiptOnSend() throws Exception { Message message = UDPGatewayTest.newSampleMessage(); Receipt receipt = gate().send(cloud, message, null); assertNotNull(receipt); assertEquals(message.getUuid(), receipt.getMessageUuid()); assertEquals(Status.PENDING, receipt.getStatus()); assertEquals("UDP", receipt.getGate()); } @Test public void shouldSendAMessageToEachPort() throws Exception { System.setProperty(UDPGateway.SYSP_UDP_GROUP, "230.31.32.33"); System.setProperty(UDPGateway.SYSP_PORT_NUM, "2727"); System.setProperty(UDPGateway.SYSP_PORT_WIDTH, "3"); Message message = UDPGatewayTest.newSampleMessage(); gate().send(cloud, message, null); List<DatagramPacket> packets = getSentPackets(); assertEquals(3, packets.size()); int port = 2727; for (DatagramPacket packet : packets) { assertPacketValid(message, packet); assertEquals(port++, packet.getPort()); assertEquals(InetAddress.getByName("230.31.32.33"), packet.getAddress()); } } @Test public void shouldStartServer() throws Exception { gate(); verify(server).start(eq(socket), anyInt()); } @Test public void shouldInvokeListenerOnMessages() throws Exception { addListenerToGateway(); Message message = UDPGatewayTest.newSampleMessage(SOMEONE, ME); simulateMessageFromNetwork(message); assertMessageReceived(message); } @Test public void shouldSplitUDPPacketsToMaxPacketSizeOrLess() throws IOException { System.setProperty(UDPGateway.SYSP_UDP_PACKET_SIZE, Integer.toString(333)); Message message = getMessageWithPayload(new BigPayload(1000)); gate().send(cloud, message, null); List<DatagramPacket> packets = getSentPackets(); for (DatagramPacket datagramPacket : packets) { assertTrue(datagramPacket.getLength() <= 333); } } @Test(expected = IOException.class) public void shouldFailWhenUnableToSplitUDPPackets() throws IOException { System.setProperty(UDPGateway.SYSP_UDP_PACKET_SIZE, Integer.toString(333)); Message message = getMessageWithPayload(new BigPayload(1000).unsplittable()); gate().send(cloud, message, null); } @Test public void shouldStopServerAndCloseSocketOnClose() throws Exception { gate().close(); verify(server).stop(); verify(socket).close(); } private Message getMessageWithPayload(final BigPayload payload) { return new MessageBuilder(Message.Type.PRS, SOMEONE, ME).with(payload).make(); } private void simulateMessageFromNetwork(Message message) { ArgumentCaptor<Listener> serverListener = ArgumentCaptor.forClass(Listener.class); verify(server).addListener(serverListener.capture()); serverListener.getValue().onMessage(message); } private void assertPacketValid(Message message, final DatagramPacket packet) { byte[] actuals = packet.getData(); byte[] expecteds = gate.serializer().toBytes(message); assertArrayEquals(expecteds, actuals); } private List<DatagramPacket> getSentPackets() throws IOException { ArgumentCaptor<DatagramPacket> packetCaptor = ArgumentCaptor.forClass(DatagramPacket.class); verify(socket, atLeastOnce()).send(packetCaptor.capture()); return packetCaptor.getAllValues(); } private Message assertMessageReceived(Message message) { assertEquals(1, messages.size()); assertEquals(message, messages.get(0)); return messages.get(0); } private void sendMessageAndIgnoreExceptions(final Message message) { try { gate().send(cloud, message, null); } catch (IOException ignore) { } } private UDPGateway gate() throws IOException { if (gate == null) gate = new UDPGateway(sockets, server, synchronousGatewayMulticaster()); return gate; } private void addListenerToGateway() throws IOException { gate().addListener(null, new Listener() { @Override public void onMessage(Message message) { messages.add(message); } }); } public static Message newSampleMessage() { final UUID uuid = new UUID(123, 456); final Iden src = new Iden(Iden.Type.AGT, uuid); final Iden dst = new Iden(Iden.Type.CLD, uuid); return newSampleMessage(src, dst); } private static Message newSampleMessage(final Iden src, final Iden dst) { return new MessageBuilder(Message.Type.APP, src, dst).make(); } static class BigPayload implements Message.Payload { private byte[] data; private boolean splittable = true; BigPayload(int size) { data = new byte[size]; } public BigPayload unsplittable() { this.splittable = false; return this; } @Override public Message.Payload[] split() { if (!splittable) return null; int size1 = data.length / 2; int size2 = data.length - size1; return new Message.Payload[]{new BigPayload(size1), new BigPayload(size2)}; } @Override public boolean process(Message message, Internal internal) { return false; } } }