Java tutorial
/* * The MIT License (MIT) * Copyright (c) 2016 Spyros Papageorgiou * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to * deal in the Software without restriction, including without limitation the * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or * sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.github.spapageo.jannel.client; import com.github.spapageo.jannel.exception.BadMessageException; import com.github.spapageo.jannel.exception.StringSizeException; import com.github.spapageo.jannel.msg.*; import com.github.spapageo.jannel.msg.enums.DataCoding; import com.github.spapageo.jannel.windowing.DuplicateKeyException; import com.github.spapageo.jannel.windowing.WindowFuture; import com.google.common.util.concurrent.Futures; import io.netty.channel.Channel; import io.netty.channel.DefaultChannelPromise; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.util.HashedWheelTimer; import io.netty.util.Timer; import org.junit.Before; import org.junit.Test; import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.io.IOException; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.util.UUID; import java.util.concurrent.*; import static org.junit.Assert.*; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; public class ClientSessionTest { private ClientSessionConfiguration clientSessionConfiguration; private ClientSession clientSession; private ScheduledExecutorService scheduledExecutorService; private NioEventLoopGroup eventExecutors; @Mock(answer = Answers.RETURNS_MOCKS) Channel channel; @Mock(answer = Answers.RETURNS_MOCKS) SessionHandler sessionHandler; final Timer timer = new HashedWheelTimer(); @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); clientSessionConfiguration = new ClientSessionConfiguration(); clientSessionConfiguration.setWindowSize(2); scheduledExecutorService = new ScheduledThreadPoolExecutor(2); clientSession = new ClientSession(clientSessionConfiguration, channel, timer, sessionHandler); eventExecutors = new NioEventLoopGroup(); } @Test public void testConstruction() throws Exception { clientSessionConfiguration.setWindowSize(50); ClientSession session = new ClientSession(clientSessionConfiguration, channel, timer, null); SocketAddress remoteSocketAddress = mock(SocketAddress.class); SocketAddress localSocketAddress = mock(SocketAddress.class); when(channel.remoteAddress()).thenReturn(remoteSocketAddress); when(channel.localAddress()).thenReturn(localSocketAddress); assertSame("Incorrect configuration", clientSessionConfiguration, session.getConfiguration()); assertSame("Incorrect channel", channel, session.getChannel()); assertEquals("Incorrect window size", clientSessionConfiguration.getWindowSize(), session.getMaxWindowSize()); assertNotNull("Null window", session.getWindow()); assertTrue("Not the default handler", session.getSessionHandler() instanceof DefaultSessionHandler); assertSame("Not the correct remote address", remoteSocketAddress, session.getRemoteAddress().get()); assertSame("Not the correct local address", localSocketAddress, session.getLocalAddress().get()); assertEquals("Default window size must be 0", 0, clientSession.getWindowSize()); } @Test public void testConstructionWithCustomSessionHandler() throws Exception { SessionHandler sessionHandler = mock(SessionHandler.class); ClientSession session = new ClientSession(clientSessionConfiguration, channel, timer, sessionHandler); assertSame("Wrong session handler", sessionHandler, session.getSessionHandler()); } @Test public void testSendAck() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Ack ack = mock(Ack.class); assertSame(promise, clientSession.sendAck(ack)); verify(channel).writeAndFlush(ack); } @Test public void testIdentifyWhenCommandIsIdentifyWritesToChannel() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Admin admin = new Admin(); admin.setBoxId("test"); admin.setAdminCommand(AdminCommand.IDENTIFY); clientSession.identify(admin); verify(channel).writeAndFlush(admin); verify(sessionHandler).fireSessionInitialized(clientSession); assertTrue(clientSession.isIdentified()); } @Test(expected = IllegalStateException.class) public void testIdentifyWhenCommandIsNotIdentifyThrows() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Admin admin = new Admin(); admin.setBoxId("test"); admin.setAdminCommand(AdminCommand.RESTART); clientSession.identify(admin); } @Test(expected = RuntimeException.class) public void testIdentifyWhenWriteFailsAndChannelIsActiveClosesChannelAndThrows() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setFailure(new IOException("test")); when(channel.writeAndFlush(any())).thenReturn(promise); when(channel.isActive()).thenReturn(true); when(channel.close()).thenReturn(promise); Admin admin = new Admin(); admin.setBoxId("test"); admin.setAdminCommand(AdminCommand.IDENTIFY); try { clientSession.identify(admin); } finally { verify(channel).writeAndFlush(admin); verify(channel).close(); assertTrue(clientSession.isClosed()); } } @Test(expected = RuntimeException.class) public void testIdentifyWhenWriteFailsAndChannelIsInactiveSetsClosedState() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setFailure(new IOException("test")); when(channel.writeAndFlush(any())).thenReturn(promise); when(channel.isActive()).thenReturn(false); Admin admin = new Admin(); admin.setBoxId("test"); admin.setAdminCommand(AdminCommand.IDENTIFY); try { clientSession.identify(admin); } finally { verify(channel).writeAndFlush(admin); verify(channel, times(0)).close(); assertTrue(clientSession.isClosed()); } } @Test public void testCloseWhenChannelIsActiveClosesChannel() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.isActive()).thenReturn(true); when(channel.close()).thenReturn(promise); clientSession.close(); verify(channel).close(); } @Test public void testCloseWhenChannelIsInactiveClosesChannel() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.isActive()).thenReturn(false); when(channel.close()).thenReturn(promise); clientSession.close(); verify(channel, times(0)).close(); } @Test public void testDestroysClosesChannelAndDestroysTheWindow() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.isActive()).thenReturn(false); when(channel.close()).thenReturn(promise); WindowFuture windowFuture = clientSession.getWindow().offer(UUID.randomUUID(), new Sms(), 5000); clientSession.destroy(); verify(channel, times(0)).close(); assertNull("Session handler must be null after destruction", clientSession.getSessionHandler()); assertTrue("The outstanding requests should be canceled", windowFuture.isCancelled()); } @Test public void testSendSmsSetsUUIDAndBoxIdWhenNull() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms("from", "to", "date", SmsType.MOBILE_TERMINATED_PUSH, DataCoding.DC_8BIT); clientSession.sendSms(sms, 5000); assertNotNull(sms.getId()); assertSame(clientSessionConfiguration.getClientId(), sms.getBoxId()); verify(channel).writeAndFlush(sms); } @Test(expected = DuplicateKeyException.class) public void testSendSmsReturnsFailedFutureWhenOfferToWindowFails() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); //add the sms so the next offer fails clientSession.getWindow().offer(sms.getId(), sms, 5000); WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 5000); assertFalse(future.isCancelled()); assertSame(sms, future.getRequest()); Futures.getChecked(future, DuplicateKeyException.class); } @Test(expected = IOException.class) public void testSendSmsReturnsFailedFutureWhenWriteFails() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setFailure(new IOException()); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 5000); Futures.getChecked(future, IOException.class); } @Test(expected = CancellationException.class) public void testSendSmsReturnsFailedFutureWhenWriteIsCancelled() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.cancel(true); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 5000); Futures.getUnchecked(future); } @Test public void testFireInboundMessageWhenIsAckAndCompletedRequestDoesNotNotifyHandler() throws Exception { Sms sms = new Sms(); sms.setId(UUID.randomUUID()); Ack message = new Ack(sms.getId()); WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 10000); clientSession.fireInboundMessage(message); final Ack ack = future.get(); assertSame(message, ack); verifyNoMoreInteractions(sessionHandler); } @Test(expected = InterruptedException.class) public void testFireInboundMessageWhenIsAckAndTimedOutRequestCallsUnexpectedAck() throws Exception { Sms sms = new Sms(); sms.setId(UUID.randomUUID()); Ack message = new Ack(sms.getId()); clientSessionConfiguration.setRequestExpiryTimeout(1); final WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 1); try { Futures.getChecked(future, InterruptedException.class); } catch (InterruptedException e) { clientSession.fireInboundMessage(message); verify(sessionHandler).fireUnexpectedAckReceived(message); throw e; } } @Test public void testFireInboundMessageWhenIsAckAndInvalidIdRequestCallsUnexpectedAck() throws Exception { Sms sms = new Sms(); sms.setId(UUID.randomUUID()); Ack message = new Ack(UUID.randomUUID()); clientSession.sendSms(sms, 100000); clientSession.fireInboundMessage(message); verify(sessionHandler).fireUnexpectedAckReceived(message); } @Test public void testSendHeartBeat() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); HeartBeat heartBeat = new HeartBeat(); assertSame(promise, clientSession.sendHeartBeat(heartBeat)); verify(channel).writeAndFlush(heartBeat); } @Test public void testFireInboundMessageWhenIsHeartBeatCallsHeadBeatReceived() throws Exception { HeartBeat heartBeat = new HeartBeat(); clientSession.fireInboundMessage(heartBeat); verify(sessionHandler).fireHeartBeatReceived(heartBeat); } @Test public void testFireInboundMessageWhenIsAdminCallsAdminReceived() throws Exception { Admin admin = new Admin(); clientSession.fireInboundMessage(admin); verify(sessionHandler).fireAdminCommandReceived(admin); } @Test public void testFireInboundMessageWhenIsSmsCallsSmsReceived() throws Exception { Sms sms = new Sms(); clientSession.fireInboundMessage(sms); verify(sessionHandler).fireSmsReceived(sms); } @Test public void testFireInboundMessageWhenIsDatagramItIsIgnored() throws Exception { Datagram datagram = new Datagram(); clientSession.fireInboundMessage(datagram); verifyNoMoreInteractions(sessionHandler, channel); } @Test public void testFireInboundMessageWhenIsUnknownItIsIgnored() throws Exception { Message message = new Message() { @Override public MessageType getType() { return MessageType.UNKNOWN; } }; clientSession.fireInboundMessage(message); verifyNoMoreInteractions(sessionHandler, channel); } @Test public void testFireExceptionCaughtThenBadMessageExceptionCallsFireBadMessage() throws Exception { BadMessageException exception = new StringSizeException("test"); clientSession.fireExceptionCaught(exception); verify(sessionHandler).fireBadMessageException(exception); } @Test public void testFireExceptionCaughtWhenBadMessageExceptionCallsFireUnknownThrowable() throws Exception { IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); clientSession.fireExceptionCaught(illegalArgumentException); verify(sessionHandler).fireUnknownThrowable(illegalArgumentException); } @Test public void testFireExceptionCaughtWhenBadMessageExceptionAndSessionClosedDoesNothing() throws Exception { IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); clientSession.close(); clientSession.fireExceptionCaught(illegalArgumentException); verifyNoMoreInteractions(sessionHandler); } @Test(expected = ClosedChannelException.class) public void testConnectionClosedFailsOutStandingConnectionAndCallUnexpectedlyClosed() throws Exception { Sms sms = new Sms(); WindowFuture<Sms, Ack> future = clientSession.sendSms(sms, 10000); clientSession.fireConnectionClosed(); try { Futures.getChecked(future, ClosedChannelException.class); } finally { verify(sessionHandler).fireChannelUnexpectedlyClosed(); } } @Test public void testConnectionClosedWhenChannelIsClosedDoesNotInvokeHandler() throws Exception { clientSession.close(); clientSession.fireConnectionClosed(); verifyNoMoreInteractions(sessionHandler); } @Test public void testSendSmsAndWaitReturnsCorrectResponse() throws Exception { final DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); final Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); final Ack expectedResponse = new Ack(); scheduledExecutorService.schedule(new Runnable() { @Override public void run() { clientSession.getWindow().complete(sms.getId(), expectedResponse); } }, 100, TimeUnit.MILLISECONDS); final Ack response = clientSession.sendSmsAndWait(sms, 5000); assertSame(expectedResponse, response); } @Test public void testSendSmsAndWaitThrowsWhenOfferToWindowTimesOut() { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); clientSessionConfiguration.setRequestExpiryTimeout(1); try { clientSession.sendSmsAndWait(sms, 1); } catch (InterruptedException e) { fail("Not the correct exception"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof TimeoutException); } } @Test public void testSendSmsAndWaitThrowsWhenOfferToWindowFails() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setSuccess(); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); //add the sms so the next offer fails clientSession.getWindow().offer(sms.getId(), sms, 5000); try { clientSession.sendSmsAndWait(sms, 5000); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof DuplicateKeyException); } } @Test public void testSendSmsAndWaitThrowsWhenWriteFails() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.setFailure(new IOException()); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); try { clientSession.sendSmsAndWait(sms, 5000); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof IOException); } } @Test(expected = CancellationException.class) public void testSendSmsAndWaitThrowsWhenWriteIsCancelled() throws Exception { DefaultChannelPromise promise = new DefaultChannelPromise(channel, eventExecutors.next()); promise.cancel(true); when(channel.writeAndFlush(any())).thenReturn(promise); Sms sms = new Sms(); sms.setId(UUID.randomUUID()); sms.setBoxId("test box"); clientSession.sendSmsAndWait(sms, 5000); } }