#1125 Create infrastructure for Limbo persistence + restore 5.2 JSON storage

- Introduce configurable storage mechanism
  - LimboPersistence wraps a LimboPersistenceHandler, of which there are multiple implementations
  - Outside of the limbo.persistence package, classes only talk to LimboPersistence
  - Restore the way of persisting to JSON from 5.2 (SeparateFilePersistenceHandler)

- Add handling for stored limbo players
  - Merge any existing LimboPlayers together with the goal of only keeping one version of a LimboPlayer: there is no way for a player to be online without triggering the creation of a LimboPlayer first, so we can guarantee that the in-memory LimboPlayer is the most up-to-date, i.e. when restoring limbo data we don't have to check against the disk.
  - Create and delete LimboPlayers at the same time when LimboPlayers are added or removed from the in-memory map

- Catch all exceptions in LimboPersistence so a handler throwing an unexpected exception does not stop the limbo process (#1070)

- Extend debug command /authme debug limbo to show LimboPlayer information on disk, too
This commit is contained in:
ljacqu 2017-03-12 18:43:37 +01:00
parent 1678901e02
commit 8557621c02
16 changed files with 733 additions and 28 deletions

View File

@ -159,6 +159,7 @@
<module name="EmptyCatchBlock">
<property name="exceptionVariableName" value="ignore|ignored"/>
</module>
<module name="MissingOverride"/>
</module>
<module name="FileTabCharacter"/>
</module>

View File

@ -6,10 +6,10 @@ import fr.xephi.authme.initialization.factory.Factory;
import org.bukkit.command.CommandSender;
import javax.inject.Inject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* Debug command main.
@ -47,7 +47,7 @@ public class DebugCommand implements ExecutableCommand {
// Lazy getter
private Map<String, DebugSection> getSections() {
if (sections == null) {
Map<String, DebugSection> sections = new HashMap<>();
Map<String, DebugSection> sections = new TreeMap<>();
for (Class<? extends DebugSection> sectionClass : sectionClasses) {
DebugSection section = debugSectionFactory.newInstance(sectionClass);
sections.put(section.getName(), section);

View File

@ -3,6 +3,7 @@ package fr.xephi.authme.command.executable.authme.debug;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.data.limbo.LimboService;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.service.BukkitService;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
@ -27,6 +28,9 @@ class LimboPlayerViewer implements DebugSection {
@Inject
private LimboService limboService;
@Inject
private LimboPersistence limboPersistence;
@Inject
private BukkitService bukkitService;
@ -50,22 +54,23 @@ class LimboPlayerViewer implements DebugSection {
return;
}
LimboPlayer limbo = limboService.getLimboPlayer(arguments.get(0));
LimboPlayer memoryLimbo = limboService.getLimboPlayer(arguments.get(0));
Player player = bukkitService.getPlayerExact(arguments.get(0));
if (limbo == null && player == null) {
LimboPlayer diskLimbo = player != null ? limboPersistence.getLimboPlayer(player) : null;
if (memoryLimbo == null && player == null) {
sender.sendMessage("No limbo info and no player online with name '" + arguments.get(0) + "'");
return;
}
sender.sendMessage(ChatColor.GOLD + "Showing limbo / player info for '" + arguments.get(0) + "'");
new InfoDisplayer(sender, limbo, player)
sender.sendMessage(ChatColor.GOLD + "Showing disk limbo / limbo / player info for '" + arguments.get(0) + "'");
new InfoDisplayer(sender, diskLimbo, memoryLimbo, player)
.sendEntry("Is op", LimboPlayer::isOperator, Player::isOp)
.sendEntry("Walk speed", LimboPlayer::getWalkSpeed, Player::getWalkSpeed)
.sendEntry("Can fly", LimboPlayer::isCanFly, Player::getAllowFlight)
.sendEntry("Fly speed", LimboPlayer::getFlySpeed, Player::getFlySpeed)
.sendEntry("Location", l -> formatLocation(l.getLocation()), p -> formatLocation(p.getLocation()))
.sendEntry("Group", LimboPlayer::getGroup, p -> "");
sender.sendMessage("Note: group is only shown for LimboPlayer");
sender.sendMessage("Note: group is not shown for Player. Use /authme debug groups");
}
/**
@ -102,25 +107,30 @@ class LimboPlayerViewer implements DebugSection {
*/
private static final class InfoDisplayer {
private final CommandSender sender;
private final Optional<LimboPlayer> limbo;
private final Optional<LimboPlayer> diskLimbo;
private final Optional<LimboPlayer> memoryLimbo;
private final Optional<Player> player;
/**
* Constructor.
*
* @param sender command sender to send the information to
* @param limbo the limbo player to get data from
* @param memoryLimbo the limbo player to get data from
* @param player the player to get data from
*/
InfoDisplayer(CommandSender sender, LimboPlayer limbo, Player player) {
InfoDisplayer(CommandSender sender, LimboPlayer diskLimbo, LimboPlayer memoryLimbo, Player player) {
this.sender = sender;
this.limbo = Optional.ofNullable(limbo);
this.diskLimbo = Optional.ofNullable(diskLimbo);
this.memoryLimbo = Optional.ofNullable(memoryLimbo);
this.player = Optional.ofNullable(player);
if (limbo == null) {
if (memoryLimbo == null) {
sender.sendMessage("Note: no Limbo information available");
} else if (player == null) {
}
if (player == null) {
sender.sendMessage("Note: player is not online");
} else if (diskLimbo == null) {
sender.sendMessage("Note: no Limbo on disk available");
}
}
@ -138,10 +148,16 @@ class LimboPlayerViewer implements DebugSection {
Function<Player, T> playerGetter) {
sender.sendMessage(
title + ": "
+ limbo.map(limboGetter).map(String::valueOf).orElse("--")
+ getData(diskLimbo, limboGetter)
+ " / "
+ player.map(playerGetter).map(String::valueOf).orElse("--"));
+ getData(memoryLimbo, limboGetter)
+ " / "
+ getData(player, playerGetter));
return this;
}
static <E, T> String getData(Optional<E> entity, Function<E, T> getter) {
return entity.map(getter).map(String::valueOf).orElse(" -- ");
}
}
}

View File

@ -1,6 +1,7 @@
package fr.xephi.authme.data.limbo;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.settings.Settings;
import org.bukkit.entity.Player;
@ -28,7 +29,10 @@ public class LimboService {
private LimboPlayerTaskManager taskManager;
@Inject
private LimboServiceHelper limboServiceHelper;
private LimboServiceHelper helper;
@Inject
private LimboPersistence persistence;
LimboService() {
}
@ -42,19 +46,25 @@ public class LimboService {
public void createLimboPlayer(Player player, boolean isRegistered) {
final String name = player.getName().toLowerCase();
LimboPlayer limboFromDisk = persistence.getLimboPlayer(player);
if (limboFromDisk != null) {
ConsoleLogger.debug("LimboPlayer for `{0}` already exists on disk", name);
}
LimboPlayer existingLimbo = entries.remove(name);
if (existingLimbo != null) {
existingLimbo.clearTasks();
ConsoleLogger.debug("LimboPlayer for `{0}` was already present", name);
ConsoleLogger.debug("LimboPlayer for `{0}` already present in memory", name);
}
LimboPlayer limboPlayer = limboServiceHelper.merge(
limboServiceHelper.createLimboPlayer(player, isRegistered), existingLimbo);
LimboPlayer limboPlayer = helper.merge(existingLimbo, limboFromDisk);
limboPlayer = helper.merge(helper.createLimboPlayer(player, isRegistered), limboPlayer);
taskManager.registerMessageTask(player, limboPlayer, isRegistered);
taskManager.registerTimeoutTask(player, limboPlayer);
limboServiceHelper.revokeLimboStates(player);
helper.revokeLimboStates(player);
entries.put(name, limboPlayer);
persistence.saveLimboPlayer(player, limboPlayer);
}
/**
@ -98,6 +108,7 @@ public class LimboService {
settings.getProperty(RESTORE_WALK_SPEED).restoreWalkSpeed(player, limbo);
limbo.clearTasks();
ConsoleLogger.debug("Restored LimboPlayer stats for `{0}`", lowerName);
persistence.removeLimboPlayer(player);
}
}

View File

@ -69,8 +69,7 @@ class LimboServiceHelper {
* <ul>
* <li><code>isOperator, allowFlight</code>: true if either limbo has true</li>
* <li><code>flySpeed, walkSpeed</code>: maximum value of either limbo player</li>
* <li><code>group</code>: from old limbo if not empty, otherwise from new limbo</li>
* <li><code>location</code>: from old limbo</li>
* <li><code>group, location</code>: from old limbo if not empty/null, otherwise from new limbo</li>
* </ul>
*
* @param newLimbo the new limbo player
@ -89,8 +88,9 @@ class LimboServiceHelper {
float flySpeed = Math.max(newLimbo.getFlySpeed(), oldLimbo.getFlySpeed());
float walkSpeed = Math.max(newLimbo.getWalkSpeed(), oldLimbo.getWalkSpeed());
String group = firstNotEmpty(newLimbo.getGroup(), oldLimbo.getGroup());
Location location = firstNotNull(oldLimbo.getLocation(), newLimbo.getLocation());
return new LimboPlayer(oldLimbo.getLocation(), isOperator, group, canFly, walkSpeed, flySpeed);
return new LimboPlayer(location, isOperator, group, canFly, walkSpeed, flySpeed);
}
private static String firstNotEmpty(String newGroup, String oldGroup) {
@ -100,4 +100,8 @@ class LimboServiceHelper {
}
return oldGroup;
}
private static Location firstNotNull(Location first, Location second) {
return first == null ? second : first;
}
}

View File

@ -0,0 +1,82 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.SettingsDependent;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import org.bukkit.entity.Player;
import javax.inject.Inject;
/**
* Handles the persistence of LimboPlayers.
*/
public class LimboPersistence implements SettingsDependent {
private final Factory<LimboPersistenceHandler> handlerFactory;
private LimboPersistenceHandler handler;
@Inject
LimboPersistence(Settings settings, Factory<LimboPersistenceHandler> handlerFactory) {
this.handlerFactory = handlerFactory;
reload(settings);
}
/**
* Retrieves the LimboPlayer for the given player if available.
*
* @param player the player to retrieve the LimboPlayer for
* @return the player's limbo player, or null if not available
*/
public LimboPlayer getLimboPlayer(Player player) {
try {
return handler.getLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not get LimboPlayer for '" + player.getName() + "'", e);
}
return null;
}
/**
* Saves the given LimboPlayer for the provided player.
*
* @param player the player to save the LimboPlayer for
* @param limbo the limbo player to save
*/
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
try {
handler.saveLimboPlayer(player, limbo);
} catch (Exception e) {
ConsoleLogger.logException("Could not save LimboPlayer for '" + player.getName() + "'", e);
}
}
/**
* Removes the LimboPlayer for the given player.
*
* @param player the player whose LimboPlayer should be removed
*/
public void removeLimboPlayer(Player player) {
try {
handler.removeLimboPlayer(player);
} catch (Exception e) {
ConsoleLogger.logException("Could not remove LimboPlayer for '" + player.getName() + "'", e);
}
}
@Override
public void reload(Settings settings) {
LimboPersistenceType persistenceType = settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE);
if (handler == null || handler.getType() != persistenceType) {
// If we're changing from an existing handler, output a quick hint that nothing is converted.
if (handler != null) {
ConsoleLogger.info("Limbo persistence type has changed! Note that the data is not converted.");
}
handler = handlerFactory.newInstance(persistenceType.getImplementationClass());
}
}
}

View File

@ -0,0 +1,39 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Handles I/O for storing LimboPlayer objects.
*/
interface LimboPersistenceHandler {
/**
* Returns the limbo player for the given player if it exists.
*
* @param player the player
* @return the stored limbo player, or null if not available
*/
LimboPlayer getLimboPlayer(Player player);
/**
* Saves the given limbo player for the given player to the disk.
*
* @param player the player to save the limbo player for
* @param limbo the limbo player to save
*/
void saveLimboPlayer(Player player, LimboPlayer limbo);
/**
* Removes the limbo player from the disk.
*
* @param player the player whose limbo player should be removed
*/
void removeLimboPlayer(Player player);
/**
* @return the type of the limbo persistence implementation
*/
LimboPersistenceType getType();
}

View File

@ -0,0 +1,29 @@
package fr.xephi.authme.data.limbo.persistence;
/**
* Types of persistence for LimboPlayer objects.
*/
public enum LimboPersistenceType {
INDIVIDUAL_FILES(SeparateFilePersistenceHandler.class),
DISABLED(NoOpPersistenceHandler.class);
private final Class<? extends LimboPersistenceHandler> implementationClass;
/**
* Constructor.
*
* @param implementationClass the implementation class
*/
LimboPersistenceType(Class<? extends LimboPersistenceHandler> implementationClass) {
this.implementationClass= implementationClass;
}
/**
* @return class implementing the persistence type
*/
public Class<? extends LimboPersistenceHandler> getImplementationClass() {
return implementationClass;
}
}

View File

@ -0,0 +1,30 @@
package fr.xephi.authme.data.limbo.persistence;
import fr.xephi.authme.data.limbo.LimboPlayer;
import org.bukkit.entity.Player;
/**
* Limbo player persistence implementation that does nothing.
*/
class NoOpPersistenceHandler implements LimboPersistenceHandler {
@Override
public LimboPlayer getLimboPlayer(Player player) {
return null;
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limbo) {
// noop
}
@Override
public void removeLimboPlayer(Player player) {
// noop
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.DISABLED;
}
}

View File

@ -0,0 +1,178 @@
package fr.xephi.authme.data.limbo.persistence;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import fr.xephi.authme.util.PlayerUtils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
/**
* Saves LimboPlayer objects as JSON into individual files.
*/
class SeparateFilePersistenceHandler implements LimboPersistenceHandler {
private final Gson gson;
private final File cacheDir;
private final BukkitService bukkitService;
@Inject
SeparateFilePersistenceHandler(@DataFolder File dataFolder, BukkitService bukkitService) {
this.bukkitService = bukkitService;
cacheDir = new File(dataFolder, "playerdata");
if (!cacheDir.exists() && !cacheDir.isDirectory() && !cacheDir.mkdir()) {
ConsoleLogger.warning("Failed to create userdata directory.");
}
gson = new GsonBuilder()
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerSerializer())
.registerTypeAdapter(LimboPlayer.class, new LimboPlayerDeserializer())
.setPrettyPrinting()
.create();
}
@Override
public LimboPlayer getLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id + File.separator + "data.json");
if (!file.exists()) {
return null;
}
try {
String str = Files.toString(file, StandardCharsets.UTF_8);
return gson.fromJson(str, LimboPlayer.class);
} catch (IOException e) {
ConsoleLogger.logException("Could not read player data on disk for '" + player.getName() + "'", e);
return null;
}
}
@Override
public void saveLimboPlayer(Player player, LimboPlayer limboPlayer) {
String id = PlayerUtils.getUUIDorName(player);
try {
File file = new File(cacheDir, id + File.separator + "data.json");
Files.createParentDirs(file);
Files.touch(file);
Files.write(gson.toJson(limboPlayer), file, StandardCharsets.UTF_8);
} catch (IOException e) {
ConsoleLogger.logException("Failed to write " + player.getName() + " data:", e);
}
}
/**
* Removes the LimboPlayer. This will delete the
* "playerdata/&lt;uuid or name&gt;/" folder from disk.
*
* @param player player to remove
*/
@Override
public void removeLimboPlayer(Player player) {
String id = PlayerUtils.getUUIDorName(player);
File file = new File(cacheDir, id);
if (file.exists()) {
FileUtils.purgeDirectory(file);
FileUtils.delete(file);
}
}
@Override
public LimboPersistenceType getType() {
return LimboPersistenceType.INDIVIDUAL_FILES;
}
private final class LimboPlayerDeserializer implements JsonDeserializer<LimboPlayer> {
@Override
public LimboPlayer deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext context) {
JsonObject jsonObject = jsonElement.getAsJsonObject();
if (jsonObject == null) {
return null;
}
Location loc = null;
String group = "";
boolean operator = false;
boolean canFly = false;
float walkSpeed = LimboPlayer.DEFAULT_WALK_SPEED;
float flySpeed = LimboPlayer.DEFAULT_FLY_SPEED;
JsonElement e;
if ((e = jsonObject.getAsJsonObject("location")) != null) {
JsonObject obj = e.getAsJsonObject();
World world = bukkitService.getWorld(obj.get("world").getAsString());
if (world != null) {
double x = obj.get("x").getAsDouble();
double y = obj.get("y").getAsDouble();
double z = obj.get("z").getAsDouble();
float yaw = obj.get("yaw").getAsFloat();
float pitch = obj.get("pitch").getAsFloat();
loc = new Location(world, x, y, z, yaw, pitch);
}
}
if ((e = jsonObject.get("group")) != null) {
group = e.getAsString();
}
if ((e = jsonObject.get("operator")) != null) {
operator = e.getAsBoolean();
}
if ((e = jsonObject.get("can-fly")) != null) {
canFly = e.getAsBoolean();
}
if ((e = jsonObject.get("walk-speed")) != null) {
walkSpeed = e.getAsFloat();
}
if ((e = jsonObject.get("fly-speed")) != null) {
flySpeed = e.getAsFloat();
}
return new LimboPlayer(loc, operator, group, canFly, walkSpeed, flySpeed);
}
}
private static final class LimboPlayerSerializer implements JsonSerializer<LimboPlayer> {
@Override
public JsonElement serialize(LimboPlayer limboPlayer, Type type,
JsonSerializationContext context) {
JsonObject obj = new JsonObject();
obj.addProperty("group", limboPlayer.getGroup());
Location loc = limboPlayer.getLocation();
JsonObject obj2 = new JsonObject();
obj2.addProperty("world", loc.getWorld().getName());
obj2.addProperty("x", loc.getX());
obj2.addProperty("y", loc.getY());
obj2.addProperty("z", loc.getZ());
obj2.addProperty("yaw", loc.getYaw());
obj2.addProperty("pitch", loc.getPitch());
obj.add("location", obj2);
obj.addProperty("operator", limboPlayer.isOperator());
obj.addProperty("can-fly", limboPlayer.isCanFly());
obj.addProperty("walk-speed", limboPlayer.getWalkSpeed());
obj.addProperty("fly-speed", limboPlayer.getFlySpeed());
return obj;
}
}
}

View File

@ -7,6 +7,7 @@ import ch.jalu.configme.properties.Property;
import com.google.common.collect.ImmutableMap;
import fr.xephi.authme.data.limbo.AllowFlightRestoreType;
import fr.xephi.authme.data.limbo.WalkFlySpeedRestoreType;
import fr.xephi.authme.data.limbo.persistence.LimboPersistenceType;
import java.util.Map;
@ -17,6 +18,15 @@ import static ch.jalu.configme.properties.PropertyInitializer.newProperty;
*/
public final class LimboSettings implements SettingsHolder {
@Comment({
"Besides storing the data in memory, you can define if/how the data should be persisted",
"on disk. This is useful in case of a server crash, so next time the server starts we can",
"properly restore things like OP status, ability to fly, and walk/fly speed.",
"DISABLED: no disk storage, INDIVIDUAL_FILES: each player data in its own file"
})
public static final Property<LimboPersistenceType> LIMBO_PERSISTENCE_TYPE =
newProperty(LimboPersistenceType.class, "limbo.persistence", LimboPersistenceType.INDIVIDUAL_FILES);
@Comment({
"Whether the player is allowed to fly: RESTORE, ENABLE, DISABLE.",
"RESTORE sets back the old property from the player."
@ -50,7 +60,7 @@ public final class LimboSettings implements SettingsHolder {
"Before a user logs in, various properties are temporarily removed from the player,",
"such as OP status, ability to fly, and walk/fly speed.",
"Once the user is logged in, we add back the properties we previously saved.",
"In this section, you may define how the properties should be restored."
"In this section, you may define how these properties should be handled."
};
return ImmutableMap.of("limbo", limboExplanation);
}

View File

@ -48,14 +48,13 @@ public class LimboServiceHelperTest {
// given
Location newLocation = mock(Location.class);
LimboPlayer newLimbo = new LimboPlayer(newLocation, false, "grp-new", true, 0.3f, 0.0f);
Location oldLocation = mock(Location.class);
LimboPlayer oldLimbo = new LimboPlayer(oldLocation, false, "", false, 0.1f, 0.1f);
LimboPlayer oldLimbo = new LimboPlayer(null, false, "", false, 0.1f, 0.1f);
// when
LimboPlayer result = limboServiceHelper.merge(newLimbo, oldLimbo);
// then
assertThat(result.getLocation(), equalTo(oldLocation));
assertThat(result.getLocation(), equalTo(newLocation));
assertThat(result.isOperator(), equalTo(false));
assertThat(result.getGroup(), equalTo("grp-new"));
assertThat(result.isCanFly(), equalTo(true));

View File

@ -4,6 +4,7 @@ import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.limbo.persistence.LimboPersistence;
import fr.xephi.authme.permission.PermissionsManager;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.SpawnLoader;
@ -57,6 +58,9 @@ public class LimboServiceTest {
@Mock
private LimboPlayerTaskManager taskManager;
@Mock
private LimboPersistence limboPersistence;
@BeforeClass
public static void initLogger() {
TestHelper.setupLogger();

View File

@ -0,0 +1,175 @@
package fr.xephi.authme.data.limbo.persistence;
import ch.jalu.injector.testing.BeforeInjecting;
import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.ReflectionTestUtils;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.factory.Factory;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.LimboSettings;
import org.bukkit.entity.Player;
import org.hamcrest.Matcher;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import java.util.logging.Logger;
import static org.hamcrest.Matchers.both;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.hamcrest.MockitoHamcrest.argThat;
/**
* Test for {@link LimboPersistence}.
*/
@RunWith(DelayedInjectionRunner.class)
public class LimboPersistenceTest {
@InjectDelayed
private LimboPersistence limboPersistence;
@Mock
private Factory<LimboPersistenceHandler> handlerFactory;
@Mock
private Settings settings;
@BeforeClass
public static void setUpLogger() {
TestHelper.setupLogger();
}
@BeforeInjecting
@SuppressWarnings("unchecked")
public void setUpMocks() {
given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE)).willReturn(LimboPersistenceType.DISABLED);
given(handlerFactory.newInstance(any(Class.class)))
.willAnswer(invocation -> mock(invocation.getArgument(0)));
}
@Test
public void shouldInitializeProperly() {
// given / when / then
assertThat(getHandler(), instanceOf(NoOpPersistenceHandler.class));
}
@Test
public void shouldDelegateToHandler() {
// given
Player player = mock(Player.class);
LimboPersistenceHandler handler = getHandler();
LimboPlayer limbo = mock(LimboPlayer.class);
given(handler.getLimboPlayer(player)).willReturn(limbo);
// when
LimboPlayer result = limboPersistence.getLimboPlayer(player);
limboPersistence.saveLimboPlayer(player, mock(LimboPlayer.class));
limboPersistence.removeLimboPlayer(mock(Player.class));
// then
assertThat(result, equalTo(limbo));
verify(handler).getLimboPlayer(player);
verify(handler).saveLimboPlayer(eq(player), argThat(notNullAndDifferentFrom(limbo)));
verify(handler).removeLimboPlayer(argThat(notNullAndDifferentFrom(player)));
}
@Test
public void shouldReloadProperly() {
// given
given(settings.getProperty(LimboSettings.LIMBO_PERSISTENCE_TYPE))
.willReturn(LimboPersistenceType.INDIVIDUAL_FILES);
// when
limboPersistence.reload(settings);
// then
assertThat(getHandler(), instanceOf(LimboPersistenceType.INDIVIDUAL_FILES.getImplementationClass()));
}
@Test
public void shouldNotReinitializeHandlerForSameType() {
// given
LimboPersistenceHandler currentHandler = getHandler();
Mockito.reset(handlerFactory);
given(currentHandler.getType()).willCallRealMethod();
// when
limboPersistence.reload(settings);
// then
verifyZeroInteractions(handlerFactory);
assertThat(currentHandler, sameInstance(getHandler()));
}
@Test
public void shouldHandleExceptionWhenGettingLimbo() {
// given
Player player = mock(Player.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(IllegalAccessException.class).when(handler).getLimboPlayer(player);
// when
LimboPlayer result = limboPersistence.getLimboPlayer(player);
// then
assertThat(result, nullValue());
verify(logger).warning(argThat(containsString("[IllegalAccessException]")));
}
@Test
public void shouldHandleExceptionWhenSavingLimbo() {
// given
Player player = mock(Player.class);
LimboPlayer limbo = mock(LimboPlayer.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(IllegalStateException.class).when(handler).saveLimboPlayer(player, limbo);
// when
limboPersistence.saveLimboPlayer(player, limbo);
// then
verify(logger).warning(argThat(containsString("[IllegalStateException]")));
}
@Test
public void shouldHandleExceptionWhenRemovingLimbo() {
// given
Player player = mock(Player.class);
Logger logger = TestHelper.setupLogger();
LimboPersistenceHandler handler = getHandler();
doThrow(UnsupportedOperationException.class).when(handler).removeLimboPlayer(player);
// when
limboPersistence.removeLimboPlayer(player);
// then
verify(logger).warning(argThat(containsString("[UnsupportedOperationException]")));
}
private LimboPersistenceHandler getHandler() {
return ReflectionTestUtils.getFieldValue(LimboPersistence.class, limboPersistence, "handler");
}
private static <T> Matcher<T> notNullAndDifferentFrom(T o) {
return both(not(sameInstance(o))).and(not(nullValue()));
}
}

View File

@ -0,0 +1,127 @@
package fr.xephi.authme.data.limbo.persistence;
import ch.jalu.injector.testing.BeforeInjecting;
import ch.jalu.injector.testing.DelayedInjectionRunner;
import ch.jalu.injector.testing.InjectDelayed;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.data.limbo.LimboPlayer;
import fr.xephi.authme.initialization.DataFolder;
import fr.xephi.authme.service.BukkitService;
import fr.xephi.authme.util.FileUtils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.UUID;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Test for {@link SeparateFilePersistenceHandler}.
*/
@RunWith(DelayedInjectionRunner.class)
public class SeparateFilePersistenceHandlerTest {
private static final UUID SAMPLE_UUID = UUID.nameUUIDFromBytes("PersistenceTest".getBytes());
private static final String SOURCE_FOLDER = TestHelper.PROJECT_ROOT + "data/backup/";
@InjectDelayed
private SeparateFilePersistenceHandler handler;
@Mock
private BukkitService bukkitService;
@DataFolder
private File dataFolder;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@BeforeInjecting
public void copyTestFiles() throws IOException {
dataFolder = temporaryFolder.newFolder();
File playerFolder = new File(dataFolder, FileUtils.makePath("playerdata", SAMPLE_UUID.toString()));
if (!playerFolder.mkdirs()) {
throw new IllegalStateException("Cannot create '" + playerFolder.getAbsolutePath() + "'");
}
Files.copy(TestHelper.getJarPath(FileUtils.makePath(SOURCE_FOLDER, "sample-folder", "data.json")),
new File(playerFolder, "data.json").toPath());
}
@Test
public void shouldReadDataFromFile() {
// given
Player player = mock(Player.class);
given(player.getUniqueId()).willReturn(SAMPLE_UUID);
World world = mock(World.class);
given(bukkitService.getWorld("nether")).willReturn(world);
// when
LimboPlayer data = handler.getLimboPlayer(player);
// then
assertThat(data, not(nullValue()));
assertThat(data.isOperator(), equalTo(true));
assertThat(data.isCanFly(), equalTo(true));
assertThat(data.getWalkSpeed(), equalTo(0.2f));
assertThat(data.getFlySpeed(), equalTo(0.1f));
assertThat(data.getGroup(), equalTo("players"));
Location location = data.getLocation();
assertThat(location.getX(), equalTo(-113.219));
assertThat(location.getY(), equalTo(72.0));
assertThat(location.getZ(), equalTo(130.637));
assertThat(location.getWorld(), equalTo(world));
assertThat(location.getPitch(), equalTo(24.15f));
assertThat(location.getYaw(), equalTo(-292.484f));
}
@Test
public void shouldReturnNullForUnavailablePlayer() {
// given
Player player = mock(Player.class);
given(player.getUniqueId()).willReturn(UUID.nameUUIDFromBytes("other-player".getBytes()));
// when
LimboPlayer data = handler.getLimboPlayer(player);
// then
assertThat(data, nullValue());
}
@Test
public void shouldSavePlayerData() {
// given
Player player = mock(Player.class);
UUID uuid = UUID.nameUUIDFromBytes("New player".getBytes());
given(player.getUniqueId()).willReturn(uuid);
World world = mock(World.class);
given(world.getName()).willReturn("player-world");
Location location = new Location(world, 0.2, 102.25, -89.28, 3.02f, 90.13f);
String group = "primary-grp";
LimboPlayer limbo = new LimboPlayer(location, true, group, true, 1.2f, 0.8f);
// when
handler.saveLimboPlayer(player, limbo);
// then
File playerFile = new File(dataFolder, FileUtils.makePath("playerdata", uuid.toString(), "data.json"));
assertThat(playerFile.exists(), equalTo(true));
// TODO ljacqu 20160711: Check contents of file
}
}

View File

@ -22,7 +22,7 @@ public class AuthMeSettingsRetrieverTest {
// an error margin of 10: this prevents us from having to adjust the test every time the config is changed.
// If this test fails, replace the first argument in closeTo() with the new number of properties
assertThat((double) configurationData.getProperties().size(),
closeTo(150, 10));
closeTo(160, 10));
}
@Test