package com.faforever.client.game;
import com.faforever.client.connectivity.ConnectivityService;
import com.faforever.client.fa.ForgedAllianceService;
import com.faforever.client.map.MapService;
import com.faforever.client.patch.GameUpdateService;
import com.faforever.client.player.PlayerInfoBeanBuilder;
import com.faforever.client.player.PlayerService;
import com.faforever.client.preferences.ForgedAlliancePrefs;
import com.faforever.client.preferences.Preferences;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.relay.LocalRelayServer;
import com.faforever.client.relay.event.RehostRequestEvent;
import com.faforever.client.remote.FafService;
import com.faforever.client.remote.domain.GameInfoMessage;
import com.faforever.client.remote.domain.GameLaunchMessage;
import com.faforever.client.remote.domain.GameTypeMessage;
import com.faforever.client.remote.domain.VictoryCondition;
import com.faforever.client.replay.ReplayService;
import com.faforever.client.test.AbstractPlainJavaFxTest;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;
import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static com.faforever.client.fa.RatingMode.GLOBAL;
import static com.faforever.client.fa.RatingMode.RANKED_1V1;
import static com.faforever.client.game.Faction.AEON;
import static com.faforever.client.game.Faction.CYBRAN;
import static com.faforever.client.remote.domain.GameState.CLOSED;
import static com.faforever.client.remote.domain.GameState.OPEN;
import static com.faforever.client.remote.domain.GameState.PLAYING;
import static com.natpryce.hamcrest.reflection.HasAnnotationMatcher.hasAnnotation;
import static java.util.Arrays.asList;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.collection.IsEmptyCollection.empty;
import static org.hamcrest.collection.IsEmptyCollection.emptyCollectionOf;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class GameServiceImplTest extends AbstractPlainJavaFxTest {
private static final long TIMEOUT = 5000;
private static final TimeUnit TIME_UNIT = TimeUnit.MILLISECONDS;
private static final int GAME_PORT = 1234;
private GameServiceImpl instance;
@Mock
private PreferencesService preferencesService;
@Mock
private FafService fafService;
@Mock
private MapService mapService;
@Mock
private ForgedAllianceService forgedAllianceService;
@Mock
private GameUpdateService gameUpdateService;
@Mock
private Preferences preferences;
@Mock
private ForgedAlliancePrefs forgedAlliancePrefs;
@Mock
private ApplicationContext applicationContext;
@Mock
private LocalRelayServer localRelayServer;
@Mock
private PlayerService playerService;
@Mock
private ConnectivityService connectivityService;
@Mock
private ScheduledExecutorService scheduledExecutorService;
@Mock
private ReplayService replayService;
@Mock
private EventBus eventBus;
@Captor
private ArgumentCaptor<Consumer<Void>> gameLaunchedListenerCaptor;
@Captor
private ArgumentCaptor<ListChangeListener.Change<? extends GameInfoBean>> gameInfoBeanChangeListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<GameTypeMessage>> gameTypeMessageListenerCaptor;
@Captor
private ArgumentCaptor<Consumer<GameInfoMessage>> gameInfoMessageListenerCaptor;
@Before
public void setUp() throws Exception {
instance = new GameServiceImpl();
instance.fafService = fafService;
instance.mapService = mapService;
instance.forgedAllianceService = forgedAllianceService;
instance.connectivityService = connectivityService;
instance.gameUpdateService = gameUpdateService;
instance.preferencesService = preferencesService;
instance.applicationContext = applicationContext;
instance.playerService = playerService;
instance.scheduledExecutorService = scheduledExecutorService;
instance.localRelayServer = localRelayServer;
instance.replayService = replayService;
instance.eventBus = eventBus;
when(preferencesService.getPreferences()).thenReturn(preferences);
when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs);
when(forgedAlliancePrefs.getPort()).thenReturn(GAME_PORT);
when(connectivityService.checkConnectivity()).thenReturn(CompletableFuture.completedFuture(null));
doAnswer(invocation -> {
try {
invocation.getArgumentAt(0, Runnable.class).run();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}).when(scheduledExecutorService).execute(any());
instance.postConstruct();
verify(fafService).addOnMessageListener(eq(GameTypeMessage.class), gameTypeMessageListenerCaptor.capture());
verify(fafService).addOnMessageListener(eq(GameInfoMessage.class), gameInfoMessageListenerCaptor.capture());
}
@Test
@SuppressWarnings("unchecked")
public void postConstruct() {
verify(fafService).addOnMessageListener(eq(GameTypeMessage.class), any(Consumer.class));
verify(fafService).addOnMessageListener(eq(GameInfoMessage.class), any(Consumer.class));
}
@Test
@SuppressWarnings("unchecked")
public void testJoinGameMapIsAvailable() throws Exception {
GameInfoBean gameInfoBean = GameInfoBeanBuilder.create().defaultValues().get();
ObservableMap<String, String> simMods = FXCollections.observableHashMap();
simMods.put("123-456-789", "Fake mod name");
gameInfoBean.setSimMods(simMods);
gameInfoBean.setMapFolderName("map");
GameLaunchMessage gameLaunchMessage = GameLaunchMessageBuilder.create().defaultValues().get();
InetSocketAddress externalSocketAddress = new InetSocketAddress(123);
when(mapService.isInstalled("map")).thenReturn(true);
when(connectivityService.getExternalSocketAddress()).thenReturn(externalSocketAddress);
when(fafService.requestJoinGame(gameInfoBean.getUid(), null)).thenReturn(completedFuture(gameLaunchMessage));
when(localRelayServer.getPort()).thenReturn(111);
when(gameUpdateService.updateInBackground(any(), any(), any(), any())).thenReturn(completedFuture(null));
CompletableFuture<Void> future = instance.joinGame(gameInfoBean, null).toCompletableFuture();
assertThat(future.get(TIMEOUT, TIME_UNIT), is(nullValue()));
verify(mapService, never()).download(any());
verify(replayService).startReplayServer(gameInfoBean.getUid());
}
@Test
public void testNoGameTypes() throws Exception {
List<GameTypeBean> gameTypes = instance.getGameTypes();
assertThat(gameTypes, emptyCollectionOf(GameTypeBean.class));
assertThat(gameTypes, hasSize(0));
}
@Test
public void testGameTypeIsOnlyAddedOnce() throws Exception {
GameTypeMessage gameTypeMessage = GameTypeInfoBuilder.create().defaultValues().get();
gameTypeMessageListenerCaptor.getValue().accept(gameTypeMessage);
gameTypeMessageListenerCaptor.getValue().accept(gameTypeMessage);
List<GameTypeBean> gameTypes = instance.getGameTypes();
assertThat(gameTypes, hasSize(1));
}
@Test
public void testDifferentGameTypes() throws Exception {
GameTypeMessage gameTypeMessage1 = GameTypeInfoBuilder.create().defaultValues().get();
GameTypeMessage gameTypeMessage2 = GameTypeInfoBuilder.create().defaultValues().get();
gameTypeMessage1.setName("number1");
gameTypeMessage2.setName("number2");
gameTypeMessageListenerCaptor.getValue().accept(gameTypeMessage1);
gameTypeMessageListenerCaptor.getValue().accept(gameTypeMessage2);
List<GameTypeBean> gameTypes = instance.getGameTypes();
assertThat(gameTypes, hasSize(2));
}
@Test
public void testAddOnGameTypeInfoListener() throws Exception {
@SuppressWarnings("unchecked")
MapChangeListener<String, GameTypeBean> listener = mock(MapChangeListener.class);
instance.addOnGameTypesChangeListener(listener);
gameTypeMessageListenerCaptor.getValue().accept(GameTypeInfoBuilder.create().defaultValues().get());
}
@Test
@SuppressWarnings("unchecked")
public void testAddOnGameStartedListener() throws Exception {
Process process = mock(Process.class);
int gpgPort = 111;
NewGameInfo newGameInfo = NewGameInfoBuilder.create().defaultValues().get();
GameLaunchMessage gameLaunchMessage = GameLaunchMessageBuilder.create().defaultValues().get();
gameLaunchMessage.setArgs(asList("/foo bar", "/bar foo"));
InetSocketAddress externalSocketAddress = new InetSocketAddress(123);
when(localRelayServer.getPort()).thenReturn(gpgPort);
when(forgedAllianceService.startGame(
gameLaunchMessage.getUid(), gameLaunchMessage.getMod(), null, asList("/foo", "bar", "/bar", "foo"), GLOBAL, gpgPort, false)
).thenReturn(process);
when(connectivityService.getExternalSocketAddress()).thenReturn(externalSocketAddress);
when(gameUpdateService.updateInBackground(any(), any(), any(), any())).thenReturn(completedFuture(null));
when(fafService.requestHostGame(newGameInfo)).thenReturn(completedFuture(gameLaunchMessage));
CountDownLatch gameStartedLatch = new CountDownLatch(1);
CountDownLatch gameTerminatedLatch = new CountDownLatch(1);
instance.gameRunningProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
gameStartedLatch.countDown();
} else {
gameTerminatedLatch.countDown();
}
});
CountDownLatch processLatch = new CountDownLatch(1);
process = mock(Process.class);
doAnswer(invocation -> {
processLatch.await();
return null;
}).when(process).waitFor();
instance.hostGame(newGameInfo).toCompletableFuture().get(TIMEOUT, TIME_UNIT);
gameStartedLatch.await(TIMEOUT, TIME_UNIT);
processLatch.countDown();
gameTerminatedLatch.await(TIMEOUT, TIME_UNIT);
verify(forgedAllianceService).startGame(
gameLaunchMessage.getUid(), gameLaunchMessage.getMod(), null, asList("/foo", "bar", "/bar", "foo"), GLOBAL,
gpgPort, false);
verify(replayService).startReplayServer(gameLaunchMessage.getUid());
}
@Test
public void testWaitForProcessTerminationInBackground() throws Exception {
instance.gameRunning.set(true);
CompletableFuture<Void> disconnectedFuture = new CompletableFuture<>();
instance.gameRunningProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
disconnectedFuture.complete(null);
}
});
Process process = mock(Process.class);
instance.spawnTerminationListener(process);
disconnectedFuture.get(5000, TimeUnit.MILLISECONDS);
verify(process).waitFor();
}
@Test
public void testOnGames() throws Exception {
assertThat(instance.getGameInfoBeans(), empty());
GameInfoMessage multiGameInfoMessage = new GameInfoMessage();
multiGameInfoMessage.setGames(asList(
GameInfoMessageBuilder.create(1).defaultValues().get(),
GameInfoMessageBuilder.create(2).defaultValues().get()
));
gameInfoMessageListenerCaptor.getValue().accept(multiGameInfoMessage);
assertThat(instance.getGameInfoBeans(), hasSize(2));
}
@Test
public void testOnGameInfoAdd() {
assertThat(instance.getGameInfoBeans(), empty());
GameInfoMessage gameInfoMessage1 = GameInfoMessageBuilder.create(1).defaultValues().title("Game 1").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage1);
GameInfoMessage gameInfoMessage2 = GameInfoMessageBuilder.create(2).defaultValues().title("Game 2").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage2);
GameInfoBean gameInfoBean1 = new GameInfoBean(gameInfoMessage1);
GameInfoBean gameInfoBean2 = new GameInfoBean(gameInfoMessage2);
assertThat(instance.getGameInfoBeans(), containsInAnyOrder(gameInfoBean1, gameInfoBean2));
}
@Test
public void testOnGameInfoMessageSetsCurrentGameIfUserIsInAndStatusOpen() throws Exception {
assertThat(instance.getCurrentGame(), nullValue());
when(playerService.getCurrentPlayer()).thenReturn(PlayerInfoBeanBuilder.create("PlayerName").get());
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1234).defaultValues()
.state(OPEN)
.addTeamMember("1", "PlayerName").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
assertThat(instance.getCurrentGame(), notNullValue());
assertThat(instance.getCurrentGame().getUid(), is(1234));
}
@Test
public void testOnGameInfoMessageDoesntSetCurrentGameIfUserIsInAndStatusNotOpen() throws Exception {
assertThat(instance.getCurrentGame(), nullValue());
when(playerService.getCurrentPlayer()).thenReturn(PlayerInfoBeanBuilder.create("PlayerName").get());
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1234).defaultValues()
.state(PLAYING)
.addTeamMember("1", "PlayerName").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
assertThat(instance.getCurrentGame(), nullValue());
}
@Test
public void testOnGameInfoMessageDoesntSetCurrentGameIfUserDoesntMatch() throws Exception {
assertThat(instance.getCurrentGame(), nullValue());
when(playerService.getCurrentPlayer()).thenReturn(PlayerInfoBeanBuilder.create("PlayerName").get());
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1234).defaultValues().addTeamMember("1", "Other").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
assertThat(instance.getCurrentGame(), nullValue());
}
@Test
public void testOnGameInfoModify() throws InterruptedException {
assertThat(instance.getGameInfoBeans(), empty());
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1).defaultValues().title("Game 1").state(PLAYING).get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
CountDownLatch changeLatch = new CountDownLatch(1);
GameInfoBean gameInfoBean = instance.getGameInfoBeans().iterator().next();
gameInfoBean.titleProperty().addListener((observable, oldValue, newValue) -> {
changeLatch.countDown();
});
gameInfoMessage = GameInfoMessageBuilder.create(1).defaultValues().title("Game 1 modified").state(PLAYING).get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
changeLatch.await();
assertEquals(gameInfoMessage.getTitle(), gameInfoBean.getTitle());
}
@Test
public void testOnGameInfoRemove() {
assertThat(instance.getGameInfoBeans(), empty());
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1).defaultValues().title("Game 1").get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
gameInfoMessage = GameInfoMessageBuilder.create(1).title("Game 1").defaultValues().state(CLOSED).get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
assertThat(instance.getGameInfoBeans(), empty());
}
@Test
public void testStartSearchRanked1v1() throws Exception {
GameLaunchMessage gameLaunchMessage = new GameLaunchMessage();
gameLaunchMessage.setMod("ladder1v1");
gameLaunchMessage.setUid(123);
gameLaunchMessage.setArgs(Collections.emptyList());
gameLaunchMessage.setMapname("scmp_037");
when(fafService.startSearchRanked1v1(CYBRAN, GAME_PORT)).thenReturn(CompletableFuture.completedFuture(gameLaunchMessage));
when(gameUpdateService.updateInBackground(GameType.LADDER_1V1.getString(), null, Collections.emptyMap(), Collections.emptySet())).thenReturn(CompletableFuture.completedFuture(null));
when(scheduledExecutorService.scheduleWithFixedDelay(any(), anyLong(), anyLong(), any())).thenReturn(mock(ScheduledFuture.class));
when(localRelayServer.getPort()).thenReturn(111);
when(mapService.isInstalled("scmp_037")).thenReturn(false);
when(mapService.download("scmp_037")).thenReturn(CompletableFuture.completedFuture(null));
CompletableFuture<Void> future = instance.startSearchRanked1v1(CYBRAN).toCompletableFuture();
verify(fafService).startSearchRanked1v1(CYBRAN, GAME_PORT);
verify(mapService).download("scmp_037");
verify(replayService).startReplayServer(123);
verify(forgedAllianceService, timeout(100)).startGame(eq(123), eq("ladder1v1"), eq(CYBRAN), eq(asList("/team", "1", "/players", "2")), eq(RANKED_1V1), anyInt(), eq(false));
assertThat(future.get(TIMEOUT, TIME_UNIT), is(nullValue()));
}
@Test
public void testStartSearchRanked1v1GameRunningDoesNothing() throws Exception {
Process process = mock(Process.class);
when(process.isAlive()).thenReturn(true);
NewGameInfo newGameInfo = NewGameInfoBuilder.create().defaultValues().get();
GameLaunchMessage gameLaunchMessage = GameLaunchMessageBuilder.create().defaultValues().get();
InetSocketAddress externalSocketAddress = new InetSocketAddress(123);
when(connectivityService.getExternalSocketAddress()).thenReturn(externalSocketAddress);
when(forgedAllianceService.startGame(anyInt(), any(), any(), any(), any(), anyInt(), eq(false))).thenReturn(process);
when(gameUpdateService.updateInBackground(any(), any(), any(), any())).thenReturn(completedFuture(null));
when(fafService.requestHostGame(newGameInfo)).thenReturn(completedFuture(gameLaunchMessage));
when(localRelayServer.getPort()).thenReturn(111);
CountDownLatch gameRunningLatch = new CountDownLatch(1);
instance.gameRunningProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
gameRunningLatch.countDown();
}
});
instance.hostGame(newGameInfo);
gameRunningLatch.await(TIMEOUT, TIME_UNIT);
instance.startSearchRanked1v1(AEON);
assertThat(instance.searching1v1Property().get(), is(false));
}
@Test
public void testStopSearchRanked1v1() throws Exception {
instance.searching1v1Property().set(true);
instance.stopSearchRanked1v1();
assertThat(instance.searching1v1Property().get(), is(false));
verify(fafService).stopSearchingRanked();
}
@Test
public void testStopSearchRanked1v1NotSearching() throws Exception {
instance.searching1v1Property().set(false);
instance.stopSearchRanked1v1();
assertThat(instance.searching1v1Property().get(), is(false));
verify(fafService, never()).stopSearchingRanked();
}
@Test
@SuppressWarnings("unchecked")
public void testAddOnGameInfoBeanListener() throws Exception {
ListChangeListener<GameInfoBean> listener = mock(ListChangeListener.class);
instance.addOnGameInfoBeansChangeListener(listener);
GameInfoMessage gameInfoMessage = GameInfoMessageBuilder.create(1).defaultValues()
.host("host")
.title("title")
.mapName("mapName")
.featuredMod("mod")
.numPlayers(2)
.maxPlayers(4)
.gameType(VictoryCondition.DOMINATION)
.state(PLAYING)
.passwordProtected(false)
.get();
gameInfoMessageListenerCaptor.getValue().accept(gameInfoMessage);
verify(listener).onChanged(gameInfoBeanChangeListenerCaptor.capture());
ListChangeListener.Change<? extends GameInfoBean> change = gameInfoBeanChangeListenerCaptor.getValue();
assertThat(change.next(), is(true));
List<? extends GameInfoBean> addedSubList = change.getAddedSubList();
assertThat(addedSubList, hasSize(1));
GameInfoBean gameInfoBean = addedSubList.get(0);
assertThat(gameInfoBean.getUid(), is(1));
assertThat(gameInfoBean.getHost(), is("host"));
assertThat(gameInfoBean.getTitle(), is("title"));
assertThat(gameInfoBean.getNumPlayers(), is(2));
assertThat(gameInfoBean.getMaxPlayers(), is(4));
assertThat(gameInfoBean.getFeaturedMod(), is("mod"));
assertThat(gameInfoBean.getVictoryCondition(), is(VictoryCondition.DOMINATION));
assertThat(gameInfoBean.getStatus(), is(PLAYING));
}
@Test
public void testSubscribeEventBus() throws Exception {
verify(eventBus).register(instance);
assertThat(ReflectionUtils.findMethod(
instance.getClass(), "onRehostRequest", RehostRequestEvent.class),
hasAnnotation(Subscribe.class));
}
@Test
public void testRehostIfGameIsNotRunning() throws Exception {
GameInfoBean gameInfoBean = GameInfoBeanBuilder.create().defaultValues().get();
instance.currentGame.set(gameInfoBean);
when(gameUpdateService.updateInBackground(any(), any(), any(), any())).thenReturn(completedFuture(null));
when(fafService.requestHostGame(any())).thenReturn(completedFuture(GameLaunchMessageBuilder.create().defaultValues().get()));
instance.onRehostRequest(new RehostRequestEvent());
verify(forgedAllianceService).startGame(anyInt(), eq("faf"), eq(null), anyListOf(String.class), eq(GLOBAL), anyInt(), eq(true));
}
@Test
public void testRehostIfGameIsRunning() throws Exception {
instance.gameRunning.set(true);
GameInfoBean gameInfoBean = GameInfoBeanBuilder.create().defaultValues().get();
instance.currentGame.set(gameInfoBean);
when(gameUpdateService.updateInBackground(any(), any(), any(), any())).thenReturn(completedFuture(null));
when(fafService.requestHostGame(any())).thenReturn(completedFuture(GameLaunchMessageBuilder.create().defaultValues().get()));
instance.onRehostRequest(new RehostRequestEvent());
verify(forgedAllianceService, never()).startGame(anyInt(), any(), any(), any(), any(), anyInt(), anyBoolean());
}
}