package com.faforever.client.connectivity; import com.faforever.client.fx.PlatformService; import com.faforever.client.i18n.I18n; import com.faforever.client.net.ConnectionState; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.PersistentNotification; import com.faforever.client.notification.Severity; import com.faforever.client.preferences.ForgedAlliancePrefs; import com.faforever.client.preferences.Preferences; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.relay.ConnectivityStateMessage; import com.faforever.client.relay.LocalRelayServer; import com.faforever.client.relay.ProcessNatPacketMessage; import com.faforever.client.relay.SendNatPacketMessage; import com.faforever.client.remote.FafService; import com.faforever.client.remote.domain.LoginMessage; import com.faforever.client.task.TaskService; import com.faforever.client.test.AbstractPlainJavaFxTest; import com.faforever.client.user.UserService; import javafx.beans.property.*; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.springframework.context.ApplicationContext; import org.springframework.util.SocketUtils; import org.testfx.util.WaitForAsyncUtils; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertArrayEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class ConnectivityServiceImplTest extends AbstractPlainJavaFxTest { private ConnectivityServiceImpl instance; @Mock private PreferencesService preferencesService; @Mock private TaskService taskService; @Mock private I18n i18n; @Mock private PlatformService platformService; @Mock private Preferences preferences; @Mock private ForgedAlliancePrefs forgedAlliancePrefs; @Mock private ApplicationContext applicationContext; @Mock private NotificationService notificationService; @Mock private FafService fafService; @Mock private LocalRelayServer localRelayServer; @Mock private ThreadPoolExecutor threadPoolExecutor; @Mock private TurnServerAccessor turnServerAccessor; @Mock private UserService userService; @Captor private ArgumentCaptor<Consumer<SendNatPacketMessage>> sendNatPacketMessageListenerCaptor; @Captor private ArgumentCaptor<Consumer<LoginMessage>> loginMessageListenerCaptor; private BooleanProperty loggedInProperty; private ObjectProperty<ConnectionState> connectionStateProperty; @Before public void setUp() throws Exception { instance = new ConnectivityServiceImpl(); instance.taskService = taskService; instance.preferencesService = preferencesService; i18n = instance.i18n = i18n; instance.platformService = platformService; instance.applicationContext = applicationContext; instance.notificationService = notificationService; instance.fafService = fafService; instance.localRelayServer = localRelayServer; instance.threadPoolExecutor = threadPoolExecutor; instance.turnServerAccessor = turnServerAccessor; instance.userService = userService; IntegerProperty portProperty = new SimpleIntegerProperty(SocketUtils.findAvailableUdpPort()); connectionStateProperty = new SimpleObjectProperty<>(); loggedInProperty = new SimpleBooleanProperty(); when(preferencesService.getPreferences()).thenReturn(preferences); when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs); when(forgedAlliancePrefs.getPort()).thenReturn(portProperty.get()); when(forgedAlliancePrefs.portProperty()).thenReturn(portProperty); when(fafService.connectionStateProperty()).thenReturn(connectionStateProperty); when(userService.loggedInProperty()).thenReturn(loggedInProperty); doAnswer(invocation -> invocation.getArgumentAt(0, Object.class)).when(taskService).submitTask(any()); doAnswer(invocation -> { WaitForAsyncUtils.async(invocation.getArgumentAt(0, Runnable.class)); return null; }).when(threadPoolExecutor).execute(any()); instance.postConstruct(); verify(fafService).addOnMessageListener(eq(SendNatPacketMessage.class), sendNatPacketMessageListenerCaptor.capture()); verify(fafService).addOnMessageListener(eq(LoginMessage.class), loginMessageListenerCaptor.capture()); } @Test public void testConnectivityStateInitiallyUnknown() throws Exception { assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); } @Test public void testConnectivityCheckTriggeredByLoginMessage() throws Exception { assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); verifyZeroInteractions(taskService); UpnpPortForwardingTask upnpPortForwardingTask = mockUpnpPortForwardingTask(); ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); loginMessageListenerCaptor.getValue().accept(new LoginMessage()); assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); verify(taskService).submitTask(upnpPortForwardingTask); verify(taskService).submitTask(connectivityCheckTask); } @Test public void testConnectivityStateResetOnDisconnect() throws Exception { mockUpnpPortForwardingTask(); ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); when(connectivityCheckTask.getFuture()).thenReturn(completedFuture(new ConnectivityStateMessage(ConnectivityState.PUBLIC, new InetSocketAddress(1337)))); when(taskService.submitTask(connectivityCheckTask)).thenReturn(connectivityCheckTask); instance.checkConnectivity(); assertThat(instance.getConnectivityState(), is(ConnectivityState.PUBLIC)); connectionStateProperty.set(ConnectionState.DISCONNECTED); assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); } private UpnpPortForwardingTask mockUpnpPortForwardingTask() { UpnpPortForwardingTask upnpPortForwardingTask = mock(UpnpPortForwardingTask.class); when(upnpPortForwardingTask.getFuture()).thenReturn(completedFuture(null)); when(applicationContext.getBean(UpnpPortForwardingTask.class)).thenReturn(upnpPortForwardingTask); return upnpPortForwardingTask; } private ConnectivityCheckTask mockConnectivityCheckTask() { ConnectivityCheckTask connectivityCheckTask = mock(ConnectivityCheckTask.class); when(applicationContext.getBean(ConnectivityCheckTask.class)).thenReturn(connectivityCheckTask); return connectivityCheckTask; } @Test(expected = IllegalStateException.class) public void testInitConnectionThrowsIseWhenConnectivityStateIsUnknown() throws Exception { ((ObjectProperty<ConnectivityState>) instance.connectivityStateProperty()).set(ConnectivityState.UNKNOWN); instance.connect(); } @Test public void testInitConnectionUsesDirectConnectionWhenConnectivityStateIsPublic() throws Exception { ((ObjectProperty<ConnectivityState>) instance.connectivityStateProperty()).set(ConnectivityState.PUBLIC); instance.connect(); verify(turnServerAccessor, never()).disconnect(); verifySendThroughPublicSocket(); verify(turnServerAccessor, never()).send(any()); } private void verifySendThroughPublicSocket() throws Exception { try (DatagramSocket socket = new DatagramSocket(new InetSocketAddress(InetAddress.getLocalHost(), 0))) { Future<DatagramPacket> packetFuture = WaitForAsyncUtils.async(() -> { DatagramPacket datagramPacket = new DatagramPacket(new byte[5], 5); socket.receive(datagramPacket); return datagramPacket; }); byte[] data = "Hello".getBytes(StandardCharsets.US_ASCII); DatagramPacket datagramPacket = new DatagramPacket(data, data.length); datagramPacket.setSocketAddress(socket.getLocalSocketAddress()); instance.send(datagramPacket); DatagramPacket packet = packetFuture.get(2, TimeUnit.SECONDS); assertArrayEquals(data, packet.getData()); } } @Test public void testInitConnectionUsesTurnWhenConnectivityStateIsStunAndPeerIsBound() throws Exception { ((ObjectProperty<ConnectivityState>) instance.connectivityStateProperty()).set(ConnectivityState.STUN); instance.connect(); verify(turnServerAccessor).addOnPacketListener(any()); verify(turnServerAccessor).connect(); when(turnServerAccessor.isBound(any())).thenReturn(true); byte[] data = "Hello".getBytes(StandardCharsets.US_ASCII); DatagramPacket datagramPacket = new DatagramPacket(data, data.length); datagramPacket.setSocketAddress(new InetSocketAddress("example.com", 1)); instance.send(datagramPacket); verify(turnServerAccessor).send(datagramPacket); } @Test public void testInitConnectionUsesDirectConnectionWhenConnectivityStateIsStunButPeerIsNotBound() throws Exception { ((ObjectProperty<ConnectivityState>) instance.connectivityStateProperty()).set(ConnectivityState.STUN); instance.connect(); verify(turnServerAccessor).addOnPacketListener(any()); verify(turnServerAccessor).connect(); when(turnServerAccessor.isBound(any())).thenReturn(false); verifySendThroughPublicSocket(); verify(turnServerAccessor, never()).send(any()); } @Test(expected = IllegalStateException.class) public void testInitConnectionThrowsExceptionWhenConnectivityStateIsBlocked() throws Exception { ((ObjectProperty<ConnectivityState>) instance.connectivityStateProperty()).set(ConnectivityState.BLOCKED); instance.connect(); verify(turnServerAccessor).addOnPacketListener(any()); verify(turnServerAccessor).connect(); DatagramPacket datagramPacket = new DatagramPacket(new byte[1], 1); datagramPacket.setSocketAddress(new InetSocketAddress("example.com", 1)); instance.send(datagramPacket); verify(turnServerAccessor).send(datagramPacket); } @Test public void testGetConnectivityStateInitiallyUnknown() throws Exception { assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); } @Test public void testSendNatPacket() throws Exception { String message = "/PLAYERID 21447 Downlord"; DatagramSocket datagramSocket = new DatagramSocket(new InetSocketAddress(InetAddress.getLocalHost(), 0)); SendNatPacketMessage sendNatPacketMessage = new SendNatPacketMessage(); sendNatPacketMessage.setPublicAddress((InetSocketAddress) datagramSocket.getLocalSocketAddress()); sendNatPacketMessage.setMessage(message); sendNatPacketMessageListenerCaptor.getValue().accept(sendNatPacketMessage); byte[] bytes = new byte[128]; DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length); datagramSocket.receive(packet); assertThat(new String(packet.getData(), 1, packet.getLength() - 1, StandardCharsets.US_ASCII), is(message)); } @Test public void testCheckGamePortInBackgroundBlockedTriggersNotification() throws Exception { ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); when(connectivityCheckTask.getFuture()).thenReturn(completedFuture(new ConnectivityStateMessage( ConnectivityState.BLOCKED, new InetSocketAddress(51111) ))); UpnpPortForwardingTask upnpPortForwardingTask = mockUpnpPortForwardingTask(); instance.checkConnectivity().toCompletableFuture().get(1, TimeUnit.SECONDS); assertThat(instance.getConnectivityState(), is(ConnectivityState.BLOCKED)); verify(taskService).submitTask(connectivityCheckTask); verify(connectivityCheckTask).setPublicPort(anyInt()); verify(taskService).submitTask(upnpPortForwardingTask); verify(upnpPortForwardingTask).setPort(anyInt()); verify(notificationService).addNotification(any(PersistentNotification.class)); assertThat(instance.getConnectivityState(), is(ConnectivityState.BLOCKED)); } @Test public void testCheckGamePortInBackgroundStunDoesntTriggerNotification() throws Exception { ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); when(connectivityCheckTask.getFuture()).thenReturn( completedFuture(new ConnectivityStateMessage( ConnectivityState.STUN, new InetSocketAddress(51111) )) ); UpnpPortForwardingTask upnpPortForwardingTask = mockUpnpPortForwardingTask(); instance.checkConnectivity().toCompletableFuture().get(1, TimeUnit.SECONDS); assertThat(instance.getConnectivityState(), is(ConnectivityState.STUN)); verify(taskService).submitTask(connectivityCheckTask); verify(taskService).submitTask(upnpPortForwardingTask); verify(upnpPortForwardingTask).setPort(anyInt()); verifyZeroInteractions(notificationService); assertThat(instance.getConnectivityState(), is(ConnectivityState.STUN)); } @Test public void testCheckGamePortInBackgroundPublicDoesntTriggerNotification() throws Exception { ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); when(connectivityCheckTask.getFuture()).thenReturn(completedFuture( new ConnectivityStateMessage(ConnectivityState.PUBLIC, new InetSocketAddress(51111))) ); UpnpPortForwardingTask upnpPortForwardingTask = mockUpnpPortForwardingTask(); instance.checkConnectivity().toCompletableFuture().get(1, TimeUnit.SECONDS); verify(taskService).submitTask(connectivityCheckTask); verify(taskService).submitTask(upnpPortForwardingTask); verify(upnpPortForwardingTask).setPort(anyInt()); verifyZeroInteractions(notificationService); assertThat(instance.getConnectivityState(), is(ConnectivityState.PUBLIC)); } @Test public void testCheckGamePortInBackgroundFailsTriggersNotification() throws Exception { ArgumentCaptor<PersistentNotification> persistentNotificationCaptor = ArgumentCaptor.forClass(PersistentNotification.class); CompletableFuture<ConnectivityStateMessage> completableFuture = new CompletableFuture<>(); completableFuture.completeExceptionally(new Exception("junit test exception")); ConnectivityCheckTask connectivityCheckTask = mockConnectivityCheckTask(); when(connectivityCheckTask.getFuture()).thenReturn(completableFuture); UpnpPortForwardingTask upnpPortForwardingTask = mockUpnpPortForwardingTask(); instance.checkConnectivity().toCompletableFuture().get(1, TimeUnit.SECONDS); verify(taskService).submitTask(connectivityCheckTask); verify(taskService).submitTask(upnpPortForwardingTask); verify(upnpPortForwardingTask).setPort(anyInt()); verify(notificationService).addNotification(persistentNotificationCaptor.capture()); assertThat(instance.getConnectivityState(), is(ConnectivityState.UNKNOWN)); assertThat(persistentNotificationCaptor.getValue().getSeverity(), is(Severity.WARN)); assertThat(persistentNotificationCaptor.getValue().getActions(), hasSize(1)); } @After public void tearDown() throws Exception { instance.preDestroy(); } @Test public void testHandleSendNatPacket() throws Exception { CompletableFuture<ProcessNatPacketMessage> messageFuture = new CompletableFuture<>(); doAnswer(invocation -> { messageFuture.complete(invocation.getArgumentAt(0, ProcessNatPacketMessage.class)); return null; }).when(fafService).sendGpgMessage(any()); InetSocketAddress publicSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), instance.getPublicSocketAddress().getPort()); try (DatagramSocket socket = new DatagramSocket(new InetSocketAddress(InetAddress.getLocalHost(), 0))) { byte[] data = "\bFoo".getBytes(StandardCharsets.US_ASCII); DatagramPacket datagramPacket = new DatagramPacket(data, data.length); datagramPacket.setSocketAddress(publicSocketAddress); socket.send(datagramPacket); } assertThat(messageFuture.get(2, TimeUnit.SECONDS).getMessage(), is("Foo")); } @Test public void testGetRelayAddress() throws Exception { verifyZeroInteractions(turnServerAccessor); instance.getRelayAddress(); verify(turnServerAccessor).getRelayAddress(); } @Test public void testAddOnPackageListener() throws Exception { CompletableFuture<DatagramPacket> packetFuture = new CompletableFuture<>(); instance.addOnPacketListener(packetFuture::complete); InetSocketAddress publicSocketAddress = new InetSocketAddress(InetAddress.getLocalHost(), instance.getPublicSocketAddress().getPort()); try (DatagramSocket socket = new DatagramSocket(new InetSocketAddress(InetAddress.getLocalHost(), 0))) { byte[] data = "Hello".getBytes(StandardCharsets.US_ASCII); DatagramPacket datagramPacket = new DatagramPacket(data, data.length); datagramPacket.setSocketAddress(publicSocketAddress); socket.send(datagramPacket); } assertThat(packetFuture.get(2, TimeUnit.SECONDS).getLength(), is(greaterThan(0))); } @Test public void testRemoveOnPackageListener() throws Exception { // No idea how to test NOT being called in async context (waiting for timeout? Meh.) instance.removeOnPacketListener(datagramPacket -> { }); } }