package com.faforever.client.remote; import com.faforever.client.game.Faction; import com.faforever.client.i18n.I18n; import com.faforever.client.legacy.FactionDeserializer; import com.faforever.client.legacy.ServerMessageSerializer; import com.faforever.client.legacy.UidService; import com.faforever.client.notification.ImmediateNotification; import com.faforever.client.notification.NotificationService; import com.faforever.client.notification.Severity; import com.faforever.client.preferences.ForgedAlliancePrefs; import com.faforever.client.preferences.LoginPrefs; import com.faforever.client.preferences.Preferences; import com.faforever.client.preferences.PreferencesService; import com.faforever.client.rankedmatch.MatchmakerMessage; import com.faforever.client.rankedmatch.SearchRanked1V1ClientMessage; import com.faforever.client.rankedmatch.StopSearchRanked1V1ClientMessage; import com.faforever.client.remote.domain.ClientMessageType; import com.faforever.client.remote.domain.FafServerMessage; import com.faforever.client.remote.domain.FafServerMessageType; import com.faforever.client.remote.domain.GameLaunchMessage; import com.faforever.client.remote.domain.GameTypeMessage; import com.faforever.client.remote.domain.InitSessionMessage; import com.faforever.client.remote.domain.LoginClientMessage; import com.faforever.client.remote.domain.LoginMessage; import com.faforever.client.remote.domain.MessageTarget; import com.faforever.client.remote.domain.NoticeMessage; import com.faforever.client.remote.domain.RatingRange; import com.faforever.client.remote.domain.SessionMessage; import com.faforever.client.remote.gson.ClientMessageTypeTypeAdapter; import com.faforever.client.remote.gson.InetSocketAddressTypeAdapter; import com.faforever.client.remote.gson.MessageTargetTypeAdapter; import com.faforever.client.remote.gson.RatingRangeTypeAdapter; import com.faforever.client.remote.gson.ServerMessageTypeTypeAdapter; import com.faforever.client.remote.io.QDataInputStream; import com.faforever.client.test.AbstractPlainJavaFxTest; import com.faforever.client.update.ClientUpdateService; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import org.apache.commons.compress.utils.IOUtils; import org.apache.maven.artifact.versioning.ComparableVersion; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testfx.util.WaitForAsyncUtils; import java.io.DataInputStream; import java.io.IOException; import java.lang.invoke.MethodHandles; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; import static java.util.Collections.singletonList; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.Matchers.empty; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class FafServerAccessorImplTest extends AbstractPlainJavaFxTest { private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private static final long TIMEOUT = 5000; private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS; private static final int GAME_PORT = 6112; private static final InetAddress LOOPBACK_ADDRESS = InetAddress.getLoopbackAddress(); private static final Gson gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) .registerTypeAdapter(ClientMessageType.class, ClientMessageTypeTypeAdapter.INSTANCE) .registerTypeAdapter(FafServerMessageType.class, ServerMessageTypeTypeAdapter.INSTANCE) .registerTypeAdapter(MessageTarget.class, MessageTargetTypeAdapter.INSTANCE) .registerTypeAdapter(Faction.class, new FactionDeserializer()) .registerTypeAdapter(InetSocketAddress.class, InetSocketAddressTypeAdapter.INSTANCE) .registerTypeAdapter(RatingRange.class, RatingRangeTypeAdapter.INSTANCE) .create(); @Rule public TemporaryFolder faDirectory = new TemporaryFolder(); @Mock private PreferencesService preferencesService; @Mock private Preferences preferences; @Mock private UidService uidService; @Mock private ForgedAlliancePrefs forgedAlliancePrefs; @Mock private ClientUpdateService clientUpdateService; @Mock private NotificationService notificationService; @Mock private I18n i18n; private FafServerAccessorImpl instance; private ServerSocket fafLobbyServerSocket; private Socket localToServerSocket; private ServerWriter serverToClientWriter; private boolean stopped; private BlockingQueue<String> messagesReceivedByFafServer; private CountDownLatch serverToClientReadyLatch; @Before public void setUp() throws Exception { serverToClientReadyLatch = new CountDownLatch(1); messagesReceivedByFafServer = new ArrayBlockingQueue<>(10); startFakeFafLobbyServer(); instance = new FafServerAccessorImpl(); instance.i18n = i18n; instance.notificationService = notificationService; instance.preferencesService = preferencesService; instance.uidService = uidService; instance.lobbyHost = LOOPBACK_ADDRESS.getHostAddress(); instance.lobbyPort = fafLobbyServerSocket.getLocalPort(); instance.clientUpdateService = clientUpdateService; LoginPrefs loginPrefs = new LoginPrefs(); loginPrefs.setUsername("junit"); loginPrefs.setPassword("password"); when(preferencesService.getPreferences()).thenReturn(preferences); when(preferencesService.getFafDataDirectory()).thenReturn(faDirectory.getRoot().toPath()); when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs); when(forgedAlliancePrefs.getPort()).thenReturn(GAME_PORT); when(preferences.getLogin()).thenReturn(loginPrefs); when(uidService.generate(any(), any())).thenReturn("encrypteduidstring"); when(clientUpdateService.getCurrentVersion()).thenReturn(new ComparableVersion("1.0")); preferencesService.getPreferences().getLogin(); } private void startFakeFafLobbyServer() throws IOException { fafLobbyServerSocket = new ServerSocket(0); logger.info("Fake server listening on " + fafLobbyServerSocket.getLocalPort()); WaitForAsyncUtils.async(() -> { try (Socket socket = fafLobbyServerSocket.accept()) { localToServerSocket = socket; QDataInputStream qDataInputStream = new QDataInputStream(new DataInputStream(socket.getInputStream())); serverToClientWriter = new ServerWriter(socket.getOutputStream()); serverToClientWriter.registerMessageSerializer(new ServerMessageSerializer(), FafServerMessage.class); serverToClientReadyLatch.countDown(); while (!stopped) { int blockSize = qDataInputStream.readInt(); String json = qDataInputStream.readQString(); if (blockSize > json.length() * 2) { // Username qDataInputStream.readQString(); // Session ID qDataInputStream.readQString(); } messagesReceivedByFafServer.add(json); } } catch (IOException e) { System.out.println("Closing fake FAF lobby server: " + e.getMessage()); throw new RuntimeException(e); } }); } @After public void tearDown() { IOUtils.closeQuietly(fafLobbyServerSocket); IOUtils.closeQuietly(localToServerSocket); } @Test public void testConnectAndLogIn() throws Exception { int playerUid = 123; String username = "JunitUser"; String password = "JunitPassword"; long sessionId = 456; CompletableFuture<LoginMessage> loginFuture = instance.connectAndLogIn(username, password).toCompletableFuture(); String json = messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT); InitSessionMessage initSessionMessage = gson.fromJson(json, InitSessionMessage.class); assertThat(initSessionMessage.getCommand(), is(ClientMessageType.ASK_SESSION)); assertThat(initSessionMessage.getVersion(), is("1.0")); assertThat(initSessionMessage.getUserAgent(), is("downlords-faf-client")); SessionMessage sessionMessage = new SessionMessage(); sessionMessage.setSession(sessionId); sendFromServer(sessionMessage); json = messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT); LoginClientMessage loginClientMessage = gson.fromJson(json, LoginClientMessage.class); assertThat(loginClientMessage.getCommand(), is(ClientMessageType.LOGIN)); assertThat(loginClientMessage.getLogin(), is(username)); assertThat(loginClientMessage.getPassword(), is(password)); assertThat(loginClientMessage.getSession(), is(sessionId)); assertThat(loginClientMessage.getUniqueId(), is("encrypteduidstring")); LoginMessage loginServerMessage = new LoginMessage(); loginServerMessage.setId(playerUid); loginServerMessage.setLogin(username); sendFromServer(loginServerMessage); LoginMessage result = loginFuture.get(TIMEOUT, TIMEOUT_UNIT); assertThat(result.getMessageType(), is(FafServerMessageType.WELCOME)); assertThat(result.getId(), is(playerUid)); assertThat(result.getLogin(), is(username)); } /** * Writes the specified message to the client as if it was sent by the FAF server. */ private void sendFromServer(FafServerMessage fafServerMessage) throws InterruptedException { serverToClientReadyLatch.await(); serverToClientWriter.write(fafServerMessage); } @Test public void testAddOnGameTypeInfoListener() throws Exception { connectAndLogIn(); CompletableFuture<GameTypeMessage> gameTypeInfoFuture = new CompletableFuture<>(); @SuppressWarnings("unchecked") Consumer<GameTypeMessage> listener = mock(Consumer.class); doAnswer(invocation -> { gameTypeInfoFuture.complete(invocation.getArgumentAt(0, GameTypeMessage.class)); return null; }).when(listener).accept(any()); instance.addOnMessageListener(GameTypeMessage.class, listener); String name = "test"; String fullname = "Test game type"; String description = "Game type description"; String icon = "what"; Boolean[] options = new Boolean[]{TRUE, FALSE, TRUE}; GameTypeMessage gameTypeMessage = new GameTypeMessage(); gameTypeMessage.setName(name); gameTypeMessage.setFullname(fullname); gameTypeMessage.setDesc(description); gameTypeMessage.setIcon(icon); gameTypeMessage.setOptions(options); sendFromServer(gameTypeMessage); GameTypeMessage result = gameTypeInfoFuture.get(TIMEOUT, TIMEOUT_UNIT); assertThat(result.getName(), is(name)); assertThat(result.getFullname(), is(fullname)); assertThat(result.getMessageType(), is(FafServerMessageType.GAME_TYPE_INFO)); assertThat(result.getDesc(), is(description)); assertThat(result.getIcon(), is(icon)); assertThat(result.getOptions(), is(options)); } private void connectAndLogIn() throws Exception { CompletableFuture<LoginMessage> loginFuture = instance.connectAndLogIn("JUnit", "JUnitPassword").toCompletableFuture(); assertNotNull(messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT)); SessionMessage sessionMessage = new SessionMessage(); sessionMessage.setSession(5678); sendFromServer(sessionMessage); assertNotNull(messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT)); LoginMessage loginServerMessage = new LoginMessage(); loginServerMessage.setId(123); loginServerMessage.setLogin("JUnitUser"); sendFromServer(loginServerMessage); assertNotNull(loginFuture.get(TIMEOUT, TIMEOUT_UNIT)); } @Test public void testRankedMatchNotification() throws Exception { connectAndLogIn(); MatchmakerMessage matchmakerMessage = new MatchmakerMessage(); matchmakerMessage.setQueues(singletonList(new MatchmakerMessage.MatchmakerQueue("ladder1v1", singletonList(new RatingRange(100, 200)), singletonList(new RatingRange(100, 200))))); CompletableFuture<MatchmakerMessage> serviceStateDoneFuture = new CompletableFuture<>(); WaitForAsyncUtils.waitForAsyncFx(200, () -> instance.addOnMessageListener( MatchmakerMessage.class, serviceStateDoneFuture::complete )); sendFromServer(matchmakerMessage); MatchmakerMessage matchmakerServerMessage = serviceStateDoneFuture.get(TIMEOUT, TIMEOUT_UNIT); assertThat(matchmakerServerMessage.getQueues(), not(empty())); } @Test public void testOnNotice() throws Exception { connectAndLogIn(); NoticeMessage noticeMessage = new NoticeMessage(); noticeMessage.setText("foo bar"); noticeMessage.setStyle("warning"); when(i18n.get("messageFromServer")).thenReturn("Message from Server"); sendFromServer(noticeMessage); ArgumentCaptor<ImmediateNotification> captor = ArgumentCaptor.forClass(ImmediateNotification.class); verify(notificationService, timeout(1000)).addNotification(captor.capture()); ImmediateNotification notification = captor.getValue(); assertThat(notification.getSeverity(), is(Severity.WARN)); assertThat(notification.getText(), is("foo bar")); assertThat(notification.getTitle(), is("Message from Server")); verify(i18n).get("messageFromServer"); } @Test public void startSearchRanked1v1WithAeon() throws Exception { connectAndLogIn(); InetSocketAddress relayAddress = InetSocketAddress.createUnresolved("foobar", 1235); CompletableFuture<GameLaunchMessage> future = instance.startSearchRanked1v1(Faction.AEON, GAME_PORT, relayAddress).toCompletableFuture(); String clientMessage = messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT); SearchRanked1V1ClientMessage searchRanked1v1Message = gson.fromJson(clientMessage, SearchRanked1V1ClientMessage.class); assertThat(searchRanked1v1Message, instanceOf(SearchRanked1V1ClientMessage.class)); assertThat(searchRanked1v1Message.getFaction(), is(Faction.AEON)); assertThat(searchRanked1v1Message.getGameport(), is(GAME_PORT)); assertThat(searchRanked1v1Message.getRelayAddress(), is(relayAddress)); GameLaunchMessage gameLaunchMessage = new GameLaunchMessage(); gameLaunchMessage.setUid(1234); sendFromServer(gameLaunchMessage); assertThat(future.get(TIMEOUT, TIMEOUT_UNIT).getUid(), is(gameLaunchMessage.getUid())); } @Test public void stopSearchingRanked1v1Match() throws Exception { connectAndLogIn(); instance.stopSearchingRanked(); String clientMessage = messagesReceivedByFafServer.poll(TIMEOUT, TIMEOUT_UNIT); StopSearchRanked1V1ClientMessage stopSearchRanked1v1Message = gson.fromJson(clientMessage, StopSearchRanked1V1ClientMessage.class); assertThat(stopSearchRanked1v1Message, instanceOf(StopSearchRanked1V1ClientMessage.class)); assertThat(stopSearchRanked1v1Message.getCommand(), is(ClientMessageType.GAME_MATCH_MAKING)); } }