package com.faforever.client.mod;
import com.faforever.client.i18n.I18n;
import com.faforever.client.io.ByteCopier;
import com.faforever.client.notification.NotificationService;
import com.faforever.client.preferences.ForgedAlliancePrefs;
import com.faforever.client.preferences.Preferences;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.remote.FafService;
import com.faforever.client.task.TaskService;
import com.faforever.client.test.AbstractPlainJavaFxTest;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import org.apache.lucene.analysis.core.SimpleAnalyzer;
import org.apache.lucene.store.RAMDirectory;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mock;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
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.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
public class ModServiceImplTest extends AbstractPlainJavaFxTest {
public static final String BLACK_OPS_UNLEASHED_DIRECTORY_NAME = "BlackOpsUnleashed";
private static final ClassPathResource BLACKOPS_SUPPORT_MOD_INFO = new ClassPathResource("/mods/blackops_support_mod_info.lua");
private static final ClassPathResource BLACKOPS_UNLEASHED_MOD_INFO = new ClassPathResource("/mods/blackops_unleashed_mod_info.lua");
private static final ClassPathResource ECO_MANAGER_MOD_INFO = new ClassPathResource("/mods/eco_manager_mod_info.lua");
private static final long TIMEOUT = 5000;
private static final TimeUnit TIMEOUT_UNIT = TimeUnit.MILLISECONDS;
@Rule
public TemporaryFolder modsDirectory = new TemporaryFolder();
@Rule
public TemporaryFolder faDataDirectory = new TemporaryFolder();
@Rule
public TemporaryFolder corruptedModsDirectory = new TemporaryFolder();
@Mock
PreferencesService preferencesService;
@Mock
private Preferences preferences;
@Mock
private ForgedAlliancePrefs forgedAlliancePrefs;
@Mock
private ApplicationContext applicationContext;
@Mock
private TaskService taskService;
@Mock
private FafService fafService;
@Mock
private NotificationService notificationService;
@Mock
private I18n i18n;
private ModServiceImpl instance;
private Path gamePrefsPath;
@Before
public void setUp() throws Exception {
instance = new ModServiceImpl();
instance.i18n = i18n;
instance.preferencesService = preferencesService;
instance.applicationContext = applicationContext;
instance.taskService = taskService;
instance.fafService = fafService;
instance.notificationService = notificationService;
instance.directory = new RAMDirectory();
instance.analyzer = new SimpleAnalyzer();
gamePrefsPath = faDataDirectory.getRoot().toPath().resolve("game.prefs");
when(preferencesService.getPreferences()).thenReturn(preferences);
when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs);
when(forgedAlliancePrefs.getPreferencesFile()).thenReturn(gamePrefsPath);
when(forgedAlliancePrefs.getModsDirectory()).thenReturn(modsDirectory.getRoot().toPath());
when(forgedAlliancePrefs.modsDirectoryProperty()).thenReturn(new SimpleObjectProperty<>(modsDirectory.getRoot().toPath()));
// FIXME how did that happen... I see this line many times but it doesn't seem to do anything useful
doAnswer(invocation -> invocation.getArgumentAt(0, Object.class)).when(taskService).submitTask(any());
copyMod(BLACK_OPS_UNLEASHED_DIRECTORY_NAME, BLACKOPS_UNLEASHED_MOD_INFO);
instance.postConstruct();
}
private void copyMod(String directoryName, ClassPathResource classPathResource) throws IOException {
Path targetDir = modsDirectory.getRoot().toPath().resolve(directoryName);
Files.createDirectories(targetDir);
try (InputStream inputStream = classPathResource.getInputStream();
OutputStream outputStream = Files.newOutputStream(targetDir.resolve("mod_info.lua"))) {
ByteCopier.from(inputStream)
.to(outputStream)
.copy();
}
}
@Test
public void testPostConstructLoadInstalledMods() throws Exception {
ObservableList<ModInfoBean> installedMods = instance.getInstalledMods();
assertThat(installedMods.size(), is(1));
}
@Test
public void testLoadInstalledModsLoadsMods() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
assertThat(instance.getInstalledMods().size(), is(1));
instance.loadInstalledMods();
assertThat(instance.getInstalledMods().size(), is(2));
}
@Test
public void testLoadInstalledModsDoesntUnloadsMods() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
instance.loadInstalledMods();
assertThat(instance.getInstalledMods().size(), is(2));
Path modDirectory = modsDirectory.getRoot().toPath().resolve("BlackOpsUnleashed");
Files.delete(modDirectory.resolve("mod_info.lua"));
Files.delete(modDirectory);
assertThat(instance.getInstalledMods().size(), is(2));
}
@Test
public void testDownloadAndInstallMod() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
InstallModTask task = mock(InstallModTask.class, withSettings().useConstructor());
when(task.getFuture()).thenReturn(completedFuture(null));
when(applicationContext.getBean(InstallModTask.class)).thenReturn(task);
URL modUrl = new URL("http://example.com/some/mod.zip");
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
assertThat(instance.getInstalledMods().size(), is(1));
instance.downloadAndInstallMod(modUrl).toCompletableFuture().get(TIMEOUT, TIMEOUT_UNIT);
verify(task).setUrl(modUrl);
assertThat(instance.getInstalledMods().size(), is(2));
}
@Test
public void testDownloadAndInstallModWithProperties() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
InstallModTask task = mock(InstallModTask.class, withSettings().useConstructor());
when(task.getFuture()).thenReturn(completedFuture(null));
when(applicationContext.getBean(InstallModTask.class)).thenReturn(task);
URL modUrl = new URL("http://example.com/some/mod.zip");
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
assertThat(instance.getInstalledMods().size(), is(1));
StringProperty stringProperty = new SimpleStringProperty();
DoubleProperty doubleProperty = new SimpleDoubleProperty();
instance.downloadAndInstallMod(modUrl, doubleProperty, stringProperty).toCompletableFuture().get(TIMEOUT, TIMEOUT_UNIT);
assertThat(stringProperty.isBound(), is(true));
assertThat(doubleProperty.isBound(), is(true));
verify(task).setUrl(modUrl);
assertThat(instance.getInstalledMods().size(), is(2));
}
@Test
public void testDownloadAndInstallModInfoBeanWithProperties() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
InstallModTask task = mock(InstallModTask.class, withSettings().useConstructor());
when(task.getFuture()).thenReturn(completedFuture(null));
when(applicationContext.getBean(InstallModTask.class)).thenReturn(task);
URL modUrl = new URL("http://example.com/some/mod.zip");
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
assertThat(instance.getInstalledMods().size(), is(1));
StringProperty stringProperty = new SimpleStringProperty();
DoubleProperty doubleProperty = new SimpleDoubleProperty();
ModInfoBean modInfoBean = ModInfoBeanBuilder.create().defaultValues().downloadUrl(modUrl).get();
instance.downloadAndInstallMod(modInfoBean, doubleProperty, stringProperty).toCompletableFuture().get(TIMEOUT, TIMEOUT_UNIT);
assertThat(stringProperty.isBound(), is(true));
assertThat(doubleProperty.isBound(), is(true));
verify(task).setUrl(modUrl);
assertThat(instance.getInstalledMods().size(), is(2));
}
@Test
public void testGetInstalledModUids() throws Exception {
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
instance.loadInstalledMods();
Set<String> installedModUids = instance.getInstalledModUids();
assertThat(installedModUids, containsInAnyOrder("9e8ea941-c306-4751-b367-f00000000005", "9e8ea941-c306-4751-b367-a11000000502"));
}
@Test
public void testGetInstalledUiModsUids() throws Exception {
assertThat(instance.getInstalledMods().size(), is(1));
copyMod("EM", ECO_MANAGER_MOD_INFO);
instance.loadInstalledMods();
assertThat(instance.getInstalledMods().size(), is(2));
Set<String> installedUiModsUids = instance.getInstalledUiModsUids();
assertThat(installedUiModsUids, contains("b2cde810-15d0-4bfa-af66-ec2d6ecd561b"));
}
@Test
public void testEnableSimModsClean() throws Exception {
Files.createFile(gamePrefsPath);
HashSet<String> simMods = new HashSet<>();
simMods.add("9e8ea941-c306-4751-b367-f00000000005");
simMods.add("9e8ea941-c306-4751-b367-a11000000502");
instance.enableSimMods(simMods);
List<String> lines = Files.readAllLines(gamePrefsPath);
assertThat(lines, contains(
"active_mods = {",
" ['9e8ea941-c306-4751-b367-f00000000005'] = true,",
" ['9e8ea941-c306-4751-b367-a11000000502'] = true",
"}"
));
}
@Test
public void testEnableSimModsModDisableUnselectedMods() throws Exception {
Iterable<? extends CharSequence> lines = Arrays.asList(
"active_mods = {",
" ['9e8ea941-c306-4751-b367-f00000000005'] = true,",
" ['9e8ea941-c306-4751-b367-a11000000502'] = true",
"}"
);
Files.write(gamePrefsPath, lines);
HashSet<String> simMods = new HashSet<>();
simMods.add("9e8ea941-c306-4751-b367-a11000000502");
instance.enableSimMods(simMods);
lines = Files.readAllLines(gamePrefsPath);
assertThat(lines, contains(
"active_mods = {",
" ['9e8ea941-c306-4751-b367-a11000000502'] = true",
"}"
));
}
@Test
public void testExtractModInfo() throws Exception {
copyMod("BlackopsSupport", BLACKOPS_SUPPORT_MOD_INFO);
copyMod("EM", ECO_MANAGER_MOD_INFO);
instance.loadInstalledMods();
ArrayList<ModInfoBean> installedMods = new ArrayList<>(instance.getInstalledMods());
Collections.sort(installedMods, (lhs, rhs) -> lhs.getName().compareTo(rhs.getName()));
ModInfoBean modInfoBean = installedMods.get(0);
assertThat(modInfoBean.getName(), is("BlackOps Global Icon Support Mod"));
assertThat(modInfoBean.getVersion(), is("5"));
assertThat(modInfoBean.getAuthor(), is("Exavier Macbeth, DeadMG"));
assertThat(modInfoBean.getDescription(), is("Version 5.0. This mod provides global icon support for any mod that places their icons in the proper folder structure. See Readme"));
assertThat(modInfoBean.getImagePath(), nullValue());
assertThat(modInfoBean.getSelectable(), is(true));
assertThat(modInfoBean.getId(), is("9e8ea941-c306-4751-b367-f00000000005"));
assertThat(modInfoBean.getUiOnly(), is(false));
modInfoBean = installedMods.get(1);
assertThat(modInfoBean.getName(), is("BlackOps Unleashed"));
assertThat(modInfoBean.getVersion(), is("8"));
assertThat(modInfoBean.getAuthor(), is("Lt_hawkeye"));
assertThat(modInfoBean.getDescription(), is("Version 5.2. BlackOps Unleased Unitpack contains several new units and game changes. Have fun"));
assertThat(modInfoBean.getImagePath(), is(modsDirectory.getRoot().toPath().resolve("BlackOpsUnleashed/icons/yoda_icon.bmp")));
assertThat(modInfoBean.getSelectable(), is(true));
assertThat(modInfoBean.getId(), is("9e8ea941-c306-4751-b367-a11000000502"));
assertThat(modInfoBean.getUiOnly(), is(false));
modInfoBean = installedMods.get(2);
assertThat(modInfoBean.getName(), is("EcoManager"));
assertThat(modInfoBean.getVersion(), is("3"));
assertThat(modInfoBean.getAuthor(), is("Crotalus"));
assertThat(modInfoBean.getDescription(), is("EcoManager v3, more efficient energy throttling"));
assertThat(modInfoBean.getImagePath(), nullValue());
assertThat(modInfoBean.getSelectable(), is(true));
assertThat(modInfoBean.getId(), is("b2cde810-15d0-4bfa-af66-ec2d6ecd561b"));
assertThat(modInfoBean.getUiOnly(), is(true));
}
@Test
public void testLoadInstalledModWithoutModInfo() throws Exception {
Files.createDirectories(modsDirectory.getRoot().toPath().resolve("foobar"));
assertThat(instance.getInstalledMods().size(), is(1));
instance.loadInstalledMods();
assertThat(instance.getInstalledMods().size(), is(1));
}
@Test
public void testIsModInstalled() throws Exception {
assertThat(instance.isModInstalled("9e8ea941-c306-4751-b367-a11000000502"), is(true));
assertThat(instance.isModInstalled("9e8ea941-c306-4751-b367-f00000000005"), is(false));
}
@Test
public void testUninstallMod() throws Exception {
UninstallModTask uninstallModTask = mock(UninstallModTask.class);
when(applicationContext.getBean(UninstallModTask.class)).thenReturn(uninstallModTask);
assertThat(instance.getInstalledMods(), hasSize(1));
instance.uninstallMod(instance.getInstalledMods().get(0));
verify(taskService).submitTask(uninstallModTask);
}
@Test
public void testGetPathForMod() throws Exception {
assertThat(instance.getInstalledMods(), hasSize(1));
Path actual = instance.getPathForMod(instance.getInstalledMods().get(0));
Path expected = modsDirectory.getRoot().toPath().resolve(BLACK_OPS_UNLEASHED_DIRECTORY_NAME);
assertThat(actual, is(expected));
}
@Test
public void testGetPathForModUnknownModReturnsNull() throws Exception {
assertThat(instance.getInstalledMods(), hasSize(1));
assertThat(instance.getPathForMod(ModInfoBeanBuilder.create().uid("1").get()), Matchers.nullValue());
}
@Test
public void testUploadMod() throws Exception {
ModUploadTask modUploadTask = mock(ModUploadTask.class);
when(applicationContext.getBean(ModUploadTask.class)).thenReturn(modUploadTask);
Path modPath = Paths.get(".");
instance.uploadMod(modPath);
verify(applicationContext).getBean(ModUploadTask.class);
verify(modUploadTask).setModPath(modPath);
verify(taskService).submitTask(modUploadTask);
}
}