mirror of
https://github.com/mcMMO-Dev/mcMMO.git
synced 2025-12-17 06:18:46 +01:00
1264 lines
54 KiB
Java
1264 lines
54 KiB
Java
package com.gmail.nossr50.database;
|
||
|
||
import static com.gmail.nossr50.util.skills.SkillTools.isChildSkill;
|
||
import static java.util.UUID.randomUUID;
|
||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
import static org.mockito.ArgumentMatchers.any;
|
||
import static org.mockito.ArgumentMatchers.anyString;
|
||
import static org.mockito.Mockito.mock;
|
||
import static org.mockito.Mockito.times;
|
||
import static org.mockito.Mockito.verify;
|
||
import static org.mockito.Mockito.when;
|
||
|
||
import com.gmail.nossr50.api.exceptions.InvalidSkillException;
|
||
import com.gmail.nossr50.database.flatfile.LeaderboardStatus;
|
||
import com.gmail.nossr50.datatypes.database.DatabaseType;
|
||
import com.gmail.nossr50.datatypes.database.PlayerStat;
|
||
import com.gmail.nossr50.datatypes.player.PlayerProfile;
|
||
import com.gmail.nossr50.datatypes.player.UniqueDataType;
|
||
import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
|
||
import com.gmail.nossr50.datatypes.skills.SuperAbilityType;
|
||
import com.gmail.nossr50.mcMMO;
|
||
import com.google.common.io.Files;
|
||
import java.io.BufferedReader;
|
||
import java.io.File;
|
||
import java.io.FileNotFoundException;
|
||
import java.io.FileReader;
|
||
import java.io.FileWriter;
|
||
import java.io.IOException;
|
||
import java.net.URI;
|
||
import java.net.URISyntaxException;
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.UUID;
|
||
import java.util.logging.Filter;
|
||
import java.util.logging.LogRecord;
|
||
import java.util.logging.Logger;
|
||
import org.bukkit.Server;
|
||
import org.bukkit.entity.Player;
|
||
import org.jetbrains.annotations.NotNull;
|
||
import org.junit.jupiter.api.AfterEach;
|
||
import org.junit.jupiter.api.BeforeAll;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.mockito.Mockito;
|
||
|
||
class FlatFileDatabaseManagerTest {
|
||
|
||
public static final @NotNull String TEST_FILE_NAME = "test.mcmmo.users";
|
||
public static final @NotNull String BAD_FILE_LINE_ONE = "mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:";
|
||
public static final @NotNull String BAD_DATA_FILE_LINE_TWENTY_THREE = "nossr51:baddata:::baddata:baddata:640:baddata:1000:1000:1000:baddata:baddata:baddata:baddata:16:0:500:20273:0:0:0:0::1000:0:0:baddata:1593543012:0:0:0:0::1000:0:0:baddata:IGNORED:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1:0:";
|
||
public static final @NotNull String DB_BADDATA = "baddatadb.users";
|
||
public static final @NotNull String DB_HEALTHY = "healthydb.users";
|
||
public static final @NotNull String HEALTHY_DB_LINE_ONE_UUID_STR = "588fe472-1c82-4c4e-9aa1-7eefccb277e3";
|
||
public static final @NotNull String DB_MISSING_LAST_LOGIN = "missinglastlogin.users";
|
||
|
||
private static File tempDir;
|
||
private static final @NotNull Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
|
||
|
||
private final long PURGE_TIME = 2_630_000_000L; // ~30 days in ms
|
||
|
||
// Making them all unique makes it easier on us to edit this stuff later
|
||
int expectedLvlMining = 1, expectedLvlWoodcutting = 2, expectedLvlRepair = 3,
|
||
expectedLvlUnarmed = 4, expectedLvlHerbalism = 5, expectedLvlExcavation = 6,
|
||
expectedLvlArchery = 7, expectedLvlSwords = 8, expectedLvlAxes = 9,
|
||
expectedLvlAcrobatics = 10, expectedLvlTaming = 11, expectedLvlFishing = 12,
|
||
expectedLvlAlchemy = 13, expectedLvlCrossbows = 14, expectedLvlTridents = 15,
|
||
expectedLvlMaces = 16, expectedLvlSpears = 17;
|
||
|
||
float expectedExpMining = 10, expectedExpWoodcutting = 20, expectedExpRepair = 30,
|
||
expectedExpUnarmed = 40, expectedExpHerbalism = 50, expectedExpExcavation = 60,
|
||
expectedExpArchery = 70, expectedExpSwords = 80, expectedExpAxes = 90,
|
||
expectedExpAcrobatics = 100, expectedExpTaming = 110, expectedExpFishing = 120,
|
||
expectedExpAlchemy = 130, expectedExpCrossbows = 140, expectedExpTridents = 150,
|
||
expectedExpMaces = 160, expectedExpSpears = 170;
|
||
|
||
long expectedBerserkCd = 111, expectedGigaDrillBreakerCd = 222, expectedTreeFellerCd = 333,
|
||
expectedGreenTerraCd = 444, expectedSerratedStrikesCd = 555,
|
||
expectedSkullSplitterCd = 666, expectedSuperBreakerCd = 777,
|
||
expectedBlastMiningCd = 888, expectedChimaeraWingCd = 999,
|
||
expectedSuperShotgunCd = 1111, expectedTridentSuperCd = 2222,
|
||
expectedExplosiveShotCd = 3333, expectedMacesSuperCd = 4444,
|
||
expectedSpearsSuperCd = 5555;
|
||
|
||
int expectedScoreboardTips = 1111;
|
||
Long expectedLastLogin = 2020L;
|
||
|
||
@BeforeAll
|
||
static void initBeforeAll() {
|
||
logger.setFilter(new DebugFilter());
|
||
// GIVEN a fully mocked mcMMO environment
|
||
mcMMO.p = Mockito.mock(mcMMO.class);
|
||
when(mcMMO.p.getLogger()).thenReturn(logger);
|
||
|
||
// Null player lookup, shouldn't affect tests
|
||
Server server = mock(Server.class);
|
||
when(mcMMO.p.getServer()).thenReturn(server);
|
||
when(server.getPlayerExact(anyString()))
|
||
.thenReturn(null);
|
||
}
|
||
|
||
@BeforeEach
|
||
void initEachTest() {
|
||
//noinspection UnstableApiUsage
|
||
tempDir = Files.createTempDir();
|
||
}
|
||
|
||
private @NotNull String getTemporaryUserFilePath() {
|
||
return tempDir.getPath() + File.separator + TEST_FILE_NAME;
|
||
}
|
||
|
||
@AfterEach
|
||
void tearDown() {
|
||
recursiveDelete(tempDir);
|
||
}
|
||
|
||
// Nothing wrong with this database
|
||
private static final String[] normalDatabaseData = {
|
||
"nossr50:1:IGNORED:IGNORED:10:2:20:3:4:5:6:7:8:9:10:30:40:50:60:70:80:90:100:IGNORED:11:110:111:222:333:444:555:666:777:IGNORED:12:120:888:IGNORED:HEARTS:13:130:588fe472-1c82-4c4e-9aa1-7eefccb277e3:1111:999:2020:140:14:150:15:1111:2222:3333:160:16:4444:170:17:5555:",
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:3030:0:0:0:0:0:0:0:0:0:0:0:0:0:",
|
||
"powerless:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:1337:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:4040:0:0:0:0:0:0:0:0:0:0:0:0:0:"
|
||
};
|
||
|
||
private static final String[] badUUIDDatabaseData = {
|
||
"nossr50:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"z750:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:3:5:1600906906:",
|
||
// This one has an incorrect UUID representation
|
||
"powerless:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:0:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:"
|
||
};
|
||
|
||
private static final String[] outdatedDatabaseData = {
|
||
"nossr50:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:",
|
||
"electronicboy:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:0:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:"
|
||
// This user is missing data added after UUID index
|
||
};
|
||
|
||
private static final String[] emptyLineDatabaseData = {
|
||
"nossr50:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:",
|
||
"kashike:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:0:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:",
|
||
"" // EMPTY LINE
|
||
};
|
||
|
||
private static final String[] emptyNameDatabaseData = {
|
||
":1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:",
|
||
"aikar:0:::0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0::0:0:0:0:0:0:0:0:0::0:0:0:0:HEARTS:0:0:e0d07db8-f7e8-43c7-9ded-864dfc6f3b7c:5:1600906906:"
|
||
};
|
||
|
||
private static final String[] duplicateNameDatabaseData = {
|
||
"mochi:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mochi:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:631e3896-da2a-4077-974b-d047859d76bc:0:0:",
|
||
};
|
||
|
||
private static final String[] duplicateUUIDDatabaseData = {
|
||
"nossr50:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mrfloris:1000:::0:1000:640:1000:1000:1000:1000:1000:1000:1000:1000:16:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
};
|
||
|
||
private static final String[] corruptDatabaseData = {
|
||
"nossr50:1000:::0:100:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:",
|
||
"corruptdataboy:の:::ののの0:2452:0:1983:1937:1790:3042ののののの:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617のののののの583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:d20c6e8d-5615-4284-b8d1-e20b92011530:5:1600906906:",
|
||
"のjapaneseuserの:333:::0:2452:0:444:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:25870f0e-7558-4659-9f60-417e24cb3332:5:1600906906:",
|
||
"sameUUIDasjapaneseuser:333:::0:442:0:544:1937:1790:3042:1138:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:25870f0e-7558-4659-9f60-417e24cb3332:5:1600906906:",
|
||
};
|
||
|
||
private static final String[] badDatabaseData = {
|
||
// First entry here is missing some values
|
||
"nossr50:1000:0:500:0:0:0:0:0::1000:0:0:0:1593543012:0:0:0:0::1000:0:0:1593806053:HEARTS:1000:0:588fe472-1c82-4c4e-9aa1-7eefccb277e3:0:0:",
|
||
// Second entry here has an integer value replaced by a string
|
||
"mrfloris:2420:::0:2452:0:1983:1937:1790:3042:badvalue:3102:2408:3411:0:0:0:0:0:0:0:0::642:0:1617583171:0:1617165043:0:1617583004:1617563189:1616785408::2184:0:0:1617852413:HEARTS:415:0:631e3896-da2a-4077-974b-d047859d76bc:5:1600906906:"
|
||
};
|
||
|
||
// ------------------------------------------------------------------------
|
||
// Core initialization / smoke tests
|
||
// ------------------------------------------------------------------------
|
||
|
||
@Test
|
||
void defaultInitCreatesDatabaseManagerAndUserFile() {
|
||
// Given + When
|
||
var databaseManager = new FlatFileDatabaseManager(getTemporaryUserFilePath(), logger, PURGE_TIME, 0);
|
||
|
||
// Then
|
||
assertNotNull(databaseManager);
|
||
assertTrue(databaseManager.getUsersFile().exists());
|
||
}
|
||
|
||
@Test
|
||
void updateLeaderboardsOnEmptyFileReturnsUpdated() {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
var status = databaseManager.updateLeaderboards();
|
||
|
||
// Then
|
||
assertEquals(LeaderboardStatus.UPDATED, status);
|
||
}
|
||
|
||
@Test
|
||
void updateLeaderboardsCalledTwiceSecondCallReturnsTooSoon() {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
var firstStatus = databaseManager.updateLeaderboards();
|
||
var secondStatus = databaseManager.updateLeaderboards();
|
||
|
||
// Then
|
||
assertEquals(LeaderboardStatus.UPDATED, firstStatus);
|
||
assertEquals(LeaderboardStatus.TOO_SOON_TO_UPDATE, secondStatus);
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// Save / load user tests
|
||
// ------------------------------------------------------------------------
|
||
|
||
@Test
|
||
void saveUserPersistsUserAndOverwritesNameOnSecondSave() {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
UUID uuid = UUID.fromString(HEALTHY_DB_LINE_ONE_UUID_STR);
|
||
String originalName = "nossr50";
|
||
var originalProfile = new PlayerProfile(originalName, uuid, 0);
|
||
|
||
// When – initial save
|
||
assertTrue(databaseManager.getUsersFile().exists());
|
||
assertTrue(databaseManager.saveUser(originalProfile));
|
||
|
||
// Then – initial load
|
||
var loadedProfile = databaseManager.loadPlayerProfile(uuid);
|
||
assertTrue(loadedProfile.isLoaded());
|
||
assertEquals(uuid, loadedProfile.getUniqueId());
|
||
assertEquals(originalName, loadedProfile.getPlayerName());
|
||
|
||
// Given – updated name
|
||
String updatedName = "changedmyname";
|
||
var updatedProfile = new PlayerProfile(updatedName, uuid, 0);
|
||
|
||
// When – overwrite
|
||
assertTrue(databaseManager.saveUser(updatedProfile));
|
||
|
||
// Then – load again should reflect updated name
|
||
var reloadedProfile = databaseManager.loadPlayerProfile(uuid);
|
||
assertTrue(reloadedProfile.isLoaded());
|
||
assertEquals(uuid, reloadedProfile.getUniqueId());
|
||
assertEquals(updatedName, reloadedProfile.getPlayerName());
|
||
}
|
||
|
||
@Test
|
||
void addedMissingLastLoginValuesAreSchemaUpgradedAndSetToMinusOne() {
|
||
// Given
|
||
File dbFile = prepareDatabaseTestResource(DB_MISSING_LAST_LOGIN);
|
||
var databaseManager = new FlatFileDatabaseManager(dbFile, logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
List<FlatFileDataFlag> flagsFound = databaseManager.checkFileHealthAndStructure();
|
||
|
||
// Then
|
||
assertNotNull(flagsFound);
|
||
assertTrue(flagsFound.contains(FlatFileDataFlag.LAST_LOGIN_SCHEMA_UPGRADE));
|
||
|
||
// And – profile last login is set to -1
|
||
var profile = databaseManager.loadPlayerProfile("nossr50");
|
||
assertEquals(-1, (long) profile.getLastLogin());
|
||
}
|
||
|
||
@Test
|
||
void loadByNameOnHealthyDatabasePopulatesAllExpectedValues() {
|
||
// Given
|
||
File healthyDbFile = prepareDatabaseTestResource(DB_HEALTHY);
|
||
var databaseManager = new FlatFileDatabaseManager(healthyDbFile, logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
List<FlatFileDataFlag> flagsFound = databaseManager.checkFileHealthAndStructure();
|
||
|
||
// Then
|
||
assertNull(flagsFound); // No flags should be found
|
||
|
||
String playerName = "nossr50";
|
||
UUID uuid = UUID.fromString(HEALTHY_DB_LINE_ONE_UUID_STR);
|
||
|
||
// And – loaded profile has all expected values
|
||
var profile = databaseManager.loadPlayerProfile(playerName);
|
||
assertHealthyDataProfileValues(playerName, uuid, profile);
|
||
}
|
||
|
||
@Test
|
||
void newUserCreatesZeroInitializedProfileAndPersistsToFile() throws IOException {
|
||
// Given
|
||
UUID uuid = new UUID(0, 1);
|
||
String playerName = "nossr50";
|
||
int startingLevel = 1337;
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, startingLevel, true);
|
||
databaseManager.checkFileHealthAndStructure();
|
||
|
||
// When – create and persist new user
|
||
var playerProfile = databaseManager.newUser(playerName, uuid);
|
||
|
||
// Then – in-memory profile
|
||
assertTrue(playerProfile.isLoaded());
|
||
assertEquals(playerName, playerProfile.getPlayerName());
|
||
assertEquals(uuid, playerProfile.getUniqueId());
|
||
|
||
// And – from disk
|
||
var profileFromDisk = databaseManager.loadPlayerProfile(uuid);
|
||
assertTrue(profileFromDisk.isLoaded());
|
||
assertEquals(playerName, profileFromDisk.getPlayerName());
|
||
assertEquals(uuid, profileFromDisk.getUniqueId());
|
||
|
||
// And – values zero-initialized except level
|
||
checkNewUserValues(playerProfile, startingLevel);
|
||
checkNewUserValues(profileFromDisk, startingLevel);
|
||
|
||
// Given – add a few more new users (including a duplicate UUID)
|
||
databaseManager.newUser("disco", new UUID(3, 3));
|
||
databaseManager.newUser("dingus", new UUID(3, 4));
|
||
databaseManager.newUser("duped_dingus", new UUID(3, 4));
|
||
|
||
// Then – there should be 5 lines (1 header + 4 players) in the DB file
|
||
final int lineCount = getSplitDataFromFile(databaseManager.getUsersFile()).size();
|
||
assertEquals(5, lineCount);
|
||
}
|
||
|
||
@Test
|
||
void addingUsersToEndOfExistingDatabaseKeepsExistingDataAndAppendsNewUsers() throws IOException {
|
||
// Given
|
||
UUID uuid = new UUID(0, 80);
|
||
String playerName = "the_kitty_man";
|
||
File file = prepareDatabaseTestResource(DB_HEALTHY);
|
||
int startingLevel = 1337;
|
||
var databaseManager = new FlatFileDatabaseManager(file, logger, PURGE_TIME, startingLevel, true);
|
||
databaseManager.checkFileHealthAndStructure();
|
||
|
||
// When – create new user against existing DB
|
||
var playerProfile = databaseManager.newUser(playerName, uuid);
|
||
|
||
// Then
|
||
assertTrue(playerProfile.isLoaded());
|
||
assertEquals(playerName, playerProfile.getPlayerName());
|
||
assertEquals(uuid, playerProfile.getUniqueId());
|
||
|
||
var profileFromDisk = databaseManager.loadPlayerProfile(uuid);
|
||
assertTrue(profileFromDisk.isLoaded());
|
||
assertEquals(playerName, profileFromDisk.getPlayerName());
|
||
assertEquals(uuid, profileFromDisk.getUniqueId());
|
||
|
||
checkNewUserValues(playerProfile, startingLevel);
|
||
checkNewUserValues(profileFromDisk, startingLevel);
|
||
|
||
// Given – add more users (with duplicate UUID)
|
||
databaseManager.newUser("bidoof", new UUID(3, 3));
|
||
databaseManager.newUser("derp", new UUID(3, 4));
|
||
databaseManager.newUser("pizza", new UUID(3, 4));
|
||
|
||
final int originalLineCount = getSplitDataFromFile(databaseManager.getUsersFile()).size();
|
||
assertEquals(7, originalLineCount);
|
||
|
||
// When – run health checker to fix duplicates
|
||
databaseManager.checkFileHealthAndStructure();
|
||
|
||
// Then – one of the duplicates should be removed
|
||
final int lineCountAfterFix = getSplitDataFromFile(databaseManager.getUsersFile()).size();
|
||
assertEquals(6, lineCountAfterFix);
|
||
}
|
||
|
||
@Test
|
||
void readLeaderboardForPowerLevelsReturnsCorrectPagedResults() throws InvalidSkillException {
|
||
// Given
|
||
var databaseManager = createDatabaseWithTwoRankedUsers();
|
||
|
||
// When – page 1 (top player only)
|
||
// Gherkin: Given a leaderboard with two users
|
||
// When we read page 1 with 1 stat per page
|
||
// Then we see the top user "leader"
|
||
List<PlayerStat> firstPage = databaseManager.readLeaderboard(null, 1, 1);
|
||
|
||
// When – page 2 (second player only)
|
||
List<PlayerStat> secondPage = databaseManager.readLeaderboard(null, 2, 1);
|
||
|
||
// When – page 3 (out of range)
|
||
List<PlayerStat> thirdPage = databaseManager.readLeaderboard(null, 3, 1);
|
||
|
||
// When – page 0 (should behave like page 1 due to Math.max)
|
||
List<PlayerStat> pageZero = databaseManager.readLeaderboard(null, 0, 10);
|
||
|
||
// Then
|
||
assertEquals(1, firstPage.size());
|
||
assertEquals("leader", firstPage.get(0).playerName());
|
||
|
||
assertEquals(1, secondPage.size());
|
||
assertEquals("follower", secondPage.get(0).playerName());
|
||
|
||
assertTrue(thirdPage.isEmpty(), "Out-of-range page should be empty");
|
||
|
||
assertFalse(pageZero.isEmpty());
|
||
assertEquals("leader", pageZero.get(0).playerName());
|
||
}
|
||
|
||
@Test
|
||
void saveUserUuidUpdatesMatchingUserAndReturnsTrue() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
replaceDataInFile(databaseManager, normalDatabaseData);
|
||
|
||
String targetUser = "nossr50";
|
||
UUID newUuid = randomUUID();
|
||
|
||
// When
|
||
// Gherkin: Given a valid flatfile entry for "nossr50"
|
||
// When we update the UUID
|
||
// Then the UUID in the file is replaced and the method returns true
|
||
boolean worked = databaseManager.saveUserUUID(targetUser, newUuid);
|
||
|
||
// Then
|
||
assertTrue(worked);
|
||
|
||
var lines = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
boolean foundNewUuid = false;
|
||
boolean oldUuidStillPresent = false;
|
||
|
||
for (String[] split : lines) {
|
||
if (split.length > FlatFileDatabaseManager.UUID_INDEX &&
|
||
targetUser.equalsIgnoreCase(split[FlatFileDatabaseManager.USERNAME_INDEX])) {
|
||
if (split[FlatFileDatabaseManager.UUID_INDEX].equals(newUuid.toString())) {
|
||
foundNewUuid = true;
|
||
}
|
||
if (split[FlatFileDatabaseManager.UUID_INDEX].equals(HEALTHY_DB_LINE_ONE_UUID_STR)) {
|
||
oldUuidStillPresent = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
assertTrue(foundNewUuid, "New UUID must be written for target user");
|
||
assertFalse(oldUuidStillPresent, "Old UUID must not remain for target user");
|
||
}
|
||
|
||
@Test
|
||
void saveUserUuidWithShortEntryDoesNotModifyDataAndReturnsFalse() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
String shortLine = "shortUser:1:2:3"; // very few fields => character.length < 42
|
||
String header = "# test header";
|
||
replaceDataInFile(databaseManager, new String[]{header, shortLine});
|
||
|
||
UUID newUuid = randomUUID();
|
||
|
||
// When
|
||
// Gherkin: Given an invalid short database entry
|
||
// When we attempt to update its UUID
|
||
// Then the method returns false and the line stays unchanged
|
||
boolean worked = databaseManager.saveUserUUID("shortUser", newUuid);
|
||
|
||
// Then
|
||
assertFalse(worked);
|
||
|
||
try (BufferedReader reader = new BufferedReader(
|
||
new FileReader(databaseManager.getUsersFile()))) {
|
||
assertEquals(header, reader.readLine());
|
||
assertEquals(shortLine, reader.readLine());
|
||
assertNull(reader.readLine());
|
||
}
|
||
}
|
||
|
||
@Test
|
||
void convertUsersCopiesAllNonCommentLinesToDestination() throws IOException {
|
||
// Given
|
||
var sourceDatabase = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
String lineOne = "# mcMMO header";
|
||
String lineTwo = "";
|
||
String lineThree = normalDatabaseData[0];
|
||
String lineFour = normalDatabaseData[1];
|
||
|
||
replaceDataInFile(sourceDatabase, new String[]{lineOne, lineTwo, lineThree, lineFour});
|
||
|
||
DatabaseManager destination = mock(DatabaseManager.class);
|
||
|
||
// When
|
||
// Gherkin: Given a flatfile with comments, empty lines, and two users
|
||
// When we convert users into another DatabaseManager
|
||
// Then saveUser is called once for each user line
|
||
sourceDatabase.convertUsers(destination);
|
||
|
||
// Then
|
||
verify(destination, times(2)).saveUser(any(PlayerProfile.class));
|
||
}
|
||
|
||
@Test
|
||
void convertUsersContinuesWhenDestinationSaveThrowsException() throws IOException {
|
||
// Given
|
||
var sourceDatabase = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
String header = "# mcMMO header";
|
||
String userLine1 = normalDatabaseData[0];
|
||
String userLine2 = normalDatabaseData[1];
|
||
|
||
replaceDataInFile(sourceDatabase, new String[]{header, userLine1, userLine2});
|
||
|
||
DatabaseManager destination = mock(DatabaseManager.class);
|
||
|
||
// First call throws, second call succeeds
|
||
Mockito.doThrow(new RuntimeException("boom"))
|
||
.when(destination)
|
||
.saveUser(any(PlayerProfile.class));
|
||
|
||
// When
|
||
// Gherkin: Given a destination that sometimes throws on save
|
||
// When we convert users
|
||
// Then conversion does not fail and all users are attempted
|
||
sourceDatabase.convertUsers(destination);
|
||
|
||
// Then
|
||
verify(destination, times(2))
|
||
.saveUser(any(PlayerProfile.class));
|
||
}
|
||
|
||
private String lineWithLastLogin(String baseLine, long lastLogin) {
|
||
String[] data = baseLine.split(":");
|
||
data[FlatFileDatabaseManager.OVERHAUL_LAST_LOGIN] = Long.toString(lastLogin);
|
||
return String.join(":", data) + ":"; // keep trailing colon similarity
|
||
}
|
||
|
||
@Test
|
||
void purgeOldUsersRemovesOnlyEntriesOlderThanPurgeTime() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
long now = System.currentTimeMillis();
|
||
|
||
// Old user: lastLogin = 0 (definitely older than PURGE_TIME)
|
||
String veryOldUser = lineWithLastLogin(normalDatabaseData[0], 0L); // username: nossr50
|
||
|
||
// Recent user: lastLogin ~ now (definitely NOT older than PURGE_TIME)
|
||
String recentUser = lineWithLastLogin(normalDatabaseData[1], now); // username: mrfloris
|
||
|
||
// Short line – not enough fields, should be preserved
|
||
String shortLine = "shortUser:1:2";
|
||
|
||
String header = "# purgeOldUsers header";
|
||
|
||
replaceDataInFile(databaseManager, new String[]{header, veryOldUser, recentUser, shortLine});
|
||
|
||
// When
|
||
// Gherkin: Given a mix of old users, recent users, comments and short lines
|
||
// When we purge old users
|
||
// Then only the truly old users are removed
|
||
databaseManager.purgeOldUsers();
|
||
|
||
// Then
|
||
List<String[]> remaining = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
List<String> remainingNames = new ArrayList<>();
|
||
|
||
for (String[] split : remaining) {
|
||
if (split.length > FlatFileDatabaseManager.USERNAME_INDEX) {
|
||
remainingNames.add(split[FlatFileDatabaseManager.USERNAME_INDEX]);
|
||
}
|
||
}
|
||
|
||
assertTrue(remainingNames.contains("mrfloris"), "Recent user must be kept");
|
||
assertTrue(remainingNames.contains("shortUser"), "Short line must be preserved");
|
||
assertFalse(remainingNames.contains("nossr50"), "Very old user must be purged");
|
||
}
|
||
|
||
@Test
|
||
void removeUserWhenUserExistsRemovesLineAndReturnsTrue() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
String header = "# removeUser header";
|
||
replaceDataInFile(databaseManager, new String[]{header,
|
||
normalDatabaseData[0], // nossr50
|
||
normalDatabaseData[1], // mrfloris
|
||
normalDatabaseData[2] // powerless
|
||
});
|
||
|
||
// When
|
||
// Gherkin: Given a database containing a user named powerless
|
||
// When we remove that user
|
||
// Then the user entry disappears from the file and the method returns true
|
||
boolean worked = databaseManager.removeUser("powerless", randomUUID());
|
||
|
||
// Then
|
||
assertTrue(worked);
|
||
|
||
List<String[]> remaining = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
List<String> remainingNames = new ArrayList<>();
|
||
for (String[] split : remaining) {
|
||
remainingNames.add(split[FlatFileDatabaseManager.USERNAME_INDEX]);
|
||
}
|
||
|
||
assertTrue(remainingNames.contains("nossr50"));
|
||
assertTrue(remainingNames.contains("mrfloris"));
|
||
assertFalse(remainingNames.contains("powerless"));
|
||
}
|
||
|
||
@Test
|
||
void removeUserWhenUserDoesNotExistReturnsFalseAndLeavesFileUnchanged() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
replaceDataInFile(databaseManager, normalDatabaseData);
|
||
List<String[]> before = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
|
||
// When
|
||
// Gherkin: Given a database that does not contain user ghostUser
|
||
// When we attempt to remove ghostUser
|
||
// Then the method returns false and the file contents are unchanged
|
||
boolean worked = databaseManager.removeUser("ghostUser", randomUUID());
|
||
|
||
// Then
|
||
assertFalse(worked);
|
||
|
||
List<String[]> after = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
assertEquals(before.size(), after.size());
|
||
|
||
for (int i = 0; i < before.size(); i++) {
|
||
assertArrayEquals(before.get(i), after.get(i));
|
||
}
|
||
}
|
||
|
||
private void checkNewUserValues(@NotNull PlayerProfile playerProfile, int startingLevel) {
|
||
// Given / Then – new user should be zero-initialized
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
continue;
|
||
}
|
||
|
||
assertEquals(startingLevel, playerProfile.getSkillLevel(primarySkillType));
|
||
assertEquals(0, playerProfile.getSkillXpLevelRaw(primarySkillType), 0);
|
||
}
|
||
|
||
for (SuperAbilityType superAbilityType : SuperAbilityType.values()) {
|
||
assertEquals(0, playerProfile.getAbilityDATS(superAbilityType));
|
||
}
|
||
|
||
assertTrue(playerProfile.getLastLogin() > 0);
|
||
assertEquals(0, playerProfile.getChimaerWingDATS());
|
||
assertEquals(0, playerProfile.getScoreboardTipsShown());
|
||
}
|
||
|
||
@Test
|
||
void loadByUUIDOnHealthyDatabaseReturnsExpectedProfile() {
|
||
// Given
|
||
File dbFile = prepareDatabaseTestResource(DB_HEALTHY);
|
||
var databaseManager = new FlatFileDatabaseManager(dbFile, logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
var flagsFound = databaseManager.checkFileHealthAndStructure();
|
||
|
||
// Then
|
||
assertNull(flagsFound); // No flags should be found
|
||
|
||
String playerName = "nossr50";
|
||
UUID uuid = UUID.fromString(HEALTHY_DB_LINE_ONE_UUID_STR);
|
||
|
||
var loadedProfile = databaseManager.loadPlayerProfile(uuid);
|
||
assertHealthyDataProfileValues(playerName, uuid, loadedProfile);
|
||
|
||
// And – unknown UUID should return an unloaded profile
|
||
assertFalse(databaseManager.loadPlayerProfile(new UUID(0, 1)).isLoaded());
|
||
}
|
||
|
||
@Test
|
||
void loadByUUIDAndNameRenamesProfileWhenNameHasChanged() {
|
||
// Given
|
||
File dbFile = prepareDatabaseTestResource(DB_HEALTHY);
|
||
var databaseManager = new FlatFileDatabaseManager(dbFile, logger, PURGE_TIME, 0, true);
|
||
List<FlatFileDataFlag> flagsFound = databaseManager.checkFileHealthAndStructure();
|
||
assertNull(flagsFound);
|
||
|
||
String originalName = "nossr50";
|
||
UUID uuid = UUID.fromString(HEALTHY_DB_LINE_ONE_UUID_STR);
|
||
Player originalPlayer = initMockPlayer(originalName, uuid);
|
||
|
||
// When – load with original name
|
||
var originalProfile = databaseManager.loadPlayerProfile(originalPlayer);
|
||
|
||
// Then
|
||
assertHealthyDataProfileValues(originalName, uuid, originalProfile);
|
||
|
||
// Given – same UUID but new name
|
||
String updatedName = "updatedName";
|
||
Player updatedPlayer = initMockPlayer(updatedName, uuid);
|
||
|
||
// When – load again
|
||
var updatedProfile = databaseManager.loadPlayerProfile(updatedPlayer);
|
||
|
||
// Then – database name should be updated to new value
|
||
assertHealthyDataProfileValues(updatedName, uuid, updatedProfile);
|
||
|
||
// And – unknown player returns unloaded profile
|
||
Player missingPlayer = initMockPlayer("doesntexist", new UUID(0, 1));
|
||
var missingProfile = databaseManager.loadPlayerProfile(missingPlayer);
|
||
assertFalse(missingProfile.isLoaded());
|
||
}
|
||
|
||
private File prepareDatabaseTestResource(@NotNull String dbFileName) {
|
||
// Given
|
||
var classLoader = getClass().getClassLoader();
|
||
URI resourceFileURI;
|
||
|
||
try {
|
||
resourceFileURI = classLoader.getResource(dbFileName).toURI();
|
||
} catch (URISyntaxException e) {
|
||
throw new RuntimeException(e);
|
||
}
|
||
|
||
// Then – resource exists
|
||
assertNotNull(resourceFileURI);
|
||
File fromResourcesFile = new File(resourceFileURI);
|
||
File copyOfFile = new File(tempDir.getPath() + File.separator + dbFileName);
|
||
|
||
if (copyOfFile.exists()) {
|
||
//noinspection ResultOfMethodCallIgnored
|
||
copyOfFile.delete();
|
||
}
|
||
|
||
assertTrue(fromResourcesFile.exists());
|
||
|
||
try {
|
||
//noinspection UnstableApiUsage
|
||
Files.copy(fromResourcesFile, copyOfFile);
|
||
} catch (IOException e) {
|
||
throw new RuntimeException(e);
|
||
}
|
||
|
||
assertNotNull(copyOfFile);
|
||
return copyOfFile;
|
||
}
|
||
|
||
private void assertHealthyDataProfileValues(@NotNull String expectedPlayerName,
|
||
@NotNull UUID expectedUuid,
|
||
@NotNull PlayerProfile profile) {
|
||
// Given / Then – profile is loaded and matches basic identity
|
||
assertTrue(profile.isLoaded());
|
||
assertEquals(expectedUuid, profile.getUniqueId());
|
||
assertEquals(expectedPlayerName, profile.getPlayerName());
|
||
|
||
// And – skill levels & XP match expected values
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
continue;
|
||
}
|
||
|
||
int expectedLevel = getExpectedLevelHealthyDBEntryOne(primarySkillType);
|
||
int actualLevel = profile.getSkillLevel(primarySkillType);
|
||
assertEquals(expectedLevel, actualLevel);
|
||
|
||
float expectedExperience = getExpectedExperienceHealthyDBEntryOne(primarySkillType);
|
||
float actualExperience = profile.getSkillXpLevelRaw(primarySkillType);
|
||
assertEquals(expectedExperience, actualExperience, 0);
|
||
}
|
||
|
||
// And – super ability cooldowns match expected values
|
||
for (SuperAbilityType superAbilityType : SuperAbilityType.values()) {
|
||
assertEquals(getExpectedSuperAbilityDATS(superAbilityType),
|
||
profile.getAbilityDATS(superAbilityType));
|
||
}
|
||
|
||
assertEquals(expectedChimaeraWingCd,
|
||
profile.getUniqueData(UniqueDataType.CHIMAERA_WING_DATS));
|
||
assertEquals(expectedScoreboardTips, profile.getScoreboardTipsShown());
|
||
assertEquals(expectedLastLogin, profile.getLastLogin());
|
||
}
|
||
|
||
private long getExpectedSuperAbilityDATS(@NotNull SuperAbilityType superAbilityType) {
|
||
return switch (superAbilityType) {
|
||
case BERSERK -> expectedBerserkCd;
|
||
case SUPER_BREAKER -> expectedSuperBreakerCd;
|
||
case GIGA_DRILL_BREAKER -> expectedGigaDrillBreakerCd;
|
||
case GREEN_TERRA -> expectedGreenTerraCd;
|
||
case SKULL_SPLITTER -> expectedSkullSplitterCd;
|
||
case SUPER_SHOTGUN -> expectedSuperShotgunCd;
|
||
case TREE_FELLER -> expectedTreeFellerCd;
|
||
case SERRATED_STRIKES -> expectedSerratedStrikesCd;
|
||
case BLAST_MINING -> expectedBlastMiningCd;
|
||
case TRIDENTS_SUPER_ABILITY -> expectedTridentSuperCd;
|
||
case EXPLOSIVE_SHOT -> expectedExplosiveShotCd;
|
||
case MACES_SUPER_ABILITY -> expectedMacesSuperCd;
|
||
case SPEARS_SUPER_ABILITY -> expectedSpearsSuperCd;
|
||
default -> throw new RuntimeException(
|
||
"Values not defined for super ability, please add " + superAbilityType);
|
||
};
|
||
}
|
||
|
||
private float getExpectedExperienceHealthyDBEntryOne(@NotNull PrimarySkillType primarySkillType) {
|
||
return switch (primarySkillType) {
|
||
case ACROBATICS -> expectedExpAcrobatics;
|
||
case ALCHEMY -> expectedExpAlchemy;
|
||
case ARCHERY -> expectedExpArchery;
|
||
case AXES -> expectedExpAxes;
|
||
case CROSSBOWS -> expectedExpCrossbows;
|
||
case EXCAVATION -> expectedExpExcavation;
|
||
case FISHING -> expectedExpFishing;
|
||
case HERBALISM -> expectedExpHerbalism;
|
||
case MINING -> expectedExpMining;
|
||
case REPAIR -> expectedExpRepair;
|
||
case SALVAGE, SMELTING -> 0;
|
||
case SWORDS -> expectedExpSwords;
|
||
case TAMING -> expectedExpTaming;
|
||
case TRIDENTS -> expectedExpTridents;
|
||
case UNARMED -> expectedExpUnarmed;
|
||
case WOODCUTTING -> expectedExpWoodcutting;
|
||
case MACES -> expectedExpMaces;
|
||
case SPEARS -> expectedExpSpears;
|
||
default -> throw new RuntimeException(
|
||
"Values for skill not defined, please add values for " + primarySkillType);
|
||
};
|
||
}
|
||
|
||
private int getExpectedLevelHealthyDBEntryOne(@NotNull PrimarySkillType primarySkillType) {
|
||
return switch (primarySkillType) {
|
||
case ACROBATICS -> expectedLvlAcrobatics;
|
||
case ALCHEMY -> expectedLvlAlchemy;
|
||
case ARCHERY -> expectedLvlArchery;
|
||
case AXES -> expectedLvlAxes;
|
||
case CROSSBOWS -> expectedLvlCrossbows;
|
||
case EXCAVATION -> expectedLvlExcavation;
|
||
case FISHING -> expectedLvlFishing;
|
||
case HERBALISM -> expectedLvlHerbalism;
|
||
case MINING -> expectedLvlMining;
|
||
case REPAIR -> expectedLvlRepair;
|
||
case SALVAGE, SMELTING -> 0;
|
||
case SWORDS -> expectedLvlSwords;
|
||
case TAMING -> expectedLvlTaming;
|
||
case TRIDENTS -> expectedLvlTridents;
|
||
case UNARMED -> expectedLvlUnarmed;
|
||
case WOODCUTTING -> expectedLvlWoodcutting;
|
||
case MACES -> expectedLvlMaces;
|
||
case SPEARS -> expectedLvlSpears;
|
||
default -> throw new RuntimeException(
|
||
"Values for skill not defined, please add values for " + primarySkillType);
|
||
};
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// File health & structure tests
|
||
// ------------------------------------------------------------------------
|
||
|
||
@Test
|
||
void overwriteName_whenDuplicateNamesExist_rewritesSecondName() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When – overwrite with duplicate name database and fix
|
||
overwriteDataAndCheckForFlag(databaseManager, duplicateNameDatabaseData,
|
||
FlatFileDataFlag.DUPLICATE_NAME);
|
||
|
||
// Then – names should no longer be equal
|
||
var splitDataLines = getSplitDataFromFile(databaseManager.getUsersFile());
|
||
assertNotEquals(splitDataLines.get(1)[0], splitDataLines.get(0)[0]);
|
||
}
|
||
|
||
@Test
|
||
void loadPlayerProfileOnMissingData_returnsUnloadedProfile() {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When
|
||
var retrievedProfile = databaseManager.loadPlayerProfile("nossr50");
|
||
|
||
// Then
|
||
assertFalse(retrievedProfile.isLoaded());
|
||
}
|
||
|
||
@Test
|
||
void purgePowerlessUsersRemovesOnlyUsersWithAllZeroSkills() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
replaceDataInFile(databaseManager, normalDatabaseData);
|
||
|
||
// When
|
||
int purgedCount = databaseManager.purgePowerlessUsers();
|
||
|
||
// Then
|
||
assertEquals(1, purgedCount); // 1 user should have been purged
|
||
}
|
||
|
||
@Test
|
||
void checkFileHealthAndStructureOnBadDatabaseReturnsNonEmptyFlags() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
replaceDataInFile(databaseManager, badDatabaseData);
|
||
|
||
// When
|
||
var dataFlags = databaseManager.checkFileHealthAndStructure();
|
||
|
||
// Then
|
||
assertNotNull(dataFlags);
|
||
assertNotEquals(0, dataFlags.size());
|
||
}
|
||
|
||
@Test
|
||
void findFixableDuplicateNamesDetectsDuplicateNameFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, duplicateNameDatabaseData,
|
||
FlatFileDataFlag.DUPLICATE_NAME);
|
||
}
|
||
|
||
@Test
|
||
void findDuplicateUUIDsDetectsDuplicateUuidFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, duplicateUUIDDatabaseData,
|
||
FlatFileDataFlag.DUPLICATE_UUID);
|
||
}
|
||
|
||
@Test
|
||
void findBadUUIDDataSetsBadUuidDataFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, badUUIDDatabaseData,
|
||
FlatFileDataFlag.BAD_UUID_DATA);
|
||
}
|
||
|
||
@Test
|
||
void findCorruptDataSetsCorruptedOrUnrecognizableFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, corruptDatabaseData,
|
||
FlatFileDataFlag.CORRUPTED_OR_UNRECOGNIZABLE);
|
||
}
|
||
|
||
@Test
|
||
void findEmptyNamesSetsMissingNameFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, emptyNameDatabaseData,
|
||
FlatFileDataFlag.MISSING_NAME);
|
||
}
|
||
|
||
@Test
|
||
void findBadValuesSetsBadValuesFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, badDatabaseData,
|
||
FlatFileDataFlag.BAD_VALUES);
|
||
}
|
||
|
||
@Test
|
||
void findOutdatedDataSetsIncompleteFlag() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
overwriteDataAndCheckForFlag(databaseManager, outdatedDatabaseData,
|
||
FlatFileDataFlag.INCOMPLETE);
|
||
}
|
||
|
||
@Test
|
||
void getDatabaseTypeReturnsFlatFileType() {
|
||
// Given / When
|
||
DatabaseManager databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// Then
|
||
assertEquals(DatabaseType.FLATFILE, databaseManager.getDatabaseType());
|
||
}
|
||
|
||
// ------------------------------------------------------------------------
|
||
// Leaderboards & ranks
|
||
// ------------------------------------------------------------------------
|
||
|
||
@Test
|
||
void readRankReturnsRanksForAllSkillsAndPowerLevel() {
|
||
// Given – empty DB and two users with different levels
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
final String rankGirlName = "rankGirl";
|
||
final UUID rankGirlUUID = randomUUID();
|
||
final String rankBoyName = "rankBoy";
|
||
final UUID rankBoyUUID = randomUUID();
|
||
|
||
// Rank 1
|
||
addPlayerProfileWithLevelsAndSave(databaseManager, rankGirlName, rankGirlUUID, 100);
|
||
// Rank 2
|
||
addPlayerProfileWithLevelsAndSave(databaseManager, rankBoyName, rankBoyUUID, 10);
|
||
|
||
// When
|
||
assertEquals(LeaderboardStatus.UPDATED, databaseManager.updateLeaderboards());
|
||
final Map<PrimarySkillType, Integer> rankGirlPositions =
|
||
databaseManager.readRank(rankGirlName);
|
||
final Map<PrimarySkillType, Integer> rankBoyPositions =
|
||
databaseManager.readRank(rankBoyName);
|
||
|
||
// Then – skill ranks
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
assertNull(rankBoyPositions.get(primarySkillType));
|
||
assertNull(rankGirlPositions.get(primarySkillType));
|
||
} else {
|
||
assertEquals(1, rankGirlPositions.get(primarySkillType));
|
||
assertEquals(2, rankBoyPositions.get(primarySkillType));
|
||
}
|
||
}
|
||
|
||
// And – power level rank (null key)
|
||
assertEquals(1, databaseManager.readRank(rankGirlName).get(null));
|
||
assertEquals(2, databaseManager.readRank(rankBoyName).get(null));
|
||
}
|
||
|
||
@Test
|
||
void readLeaderboardChildSkillThrowsInvalidSkillException() {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// When / Then
|
||
assertThrows(InvalidSkillException.class, () ->
|
||
databaseManager.readLeaderboard(PrimarySkillType.SALVAGE, 1, 10));
|
||
}
|
||
|
||
@Test
|
||
void getStoredUsersReturnsAllUsernamesFromFlatFile() throws IOException {
|
||
// Given
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
replaceDataInFile(databaseManager, normalDatabaseData);
|
||
|
||
// When
|
||
List<String> storedUsers = databaseManager.getStoredUsers();
|
||
|
||
// Then
|
||
assertEquals(List.of("nossr50", "mrfloris", "powerless"), storedUsers);
|
||
}
|
||
|
||
@Test
|
||
void loadFromFileWithBadDataFileSetsBadValuesFlag() throws URISyntaxException, IOException {
|
||
// Given
|
||
ClassLoader classLoader = getClass().getClassLoader();
|
||
URI resourceFileURI = classLoader.getResource(DB_BADDATA).toURI();
|
||
File fromResourcesFile = new File(resourceFileURI);
|
||
File copyOfFile = new File(tempDir.getPath() + File.separator + DB_BADDATA);
|
||
|
||
if (copyOfFile.exists()) {
|
||
copyOfFile.delete();
|
||
}
|
||
|
||
assertTrue(fromResourcesFile.exists());
|
||
Files.copy(fromResourcesFile, copyOfFile);
|
||
|
||
// When – read file via helper
|
||
ArrayList<String[]> dataFromFile = getSplitDataFromFile(copyOfFile);
|
||
|
||
// Then – sanity check the file contents
|
||
logger.info("File Path: " + copyOfFile.getAbsolutePath());
|
||
assertArrayEquals(BAD_FILE_LINE_ONE.split(":"), dataFromFile.get(0));
|
||
assertEquals("nossr51", dataFromFile.get(22)[0]);
|
||
assertArrayEquals(BAD_DATA_FILE_LINE_TWENTY_THREE.split(":"), dataFromFile.get(22));
|
||
|
||
// And – health check should contain BAD_VALUES flag
|
||
var databaseManager = new FlatFileDatabaseManager(copyOfFile, logger, PURGE_TIME, 0, true);
|
||
List<FlatFileDataFlag> flagsFound = databaseManager.checkFileHealthAndStructure();
|
||
assertNotNull(flagsFound);
|
||
assertTrue(flagsFound.contains(FlatFileDataFlag.BAD_VALUES));
|
||
}
|
||
|
||
private @NotNull ArrayList<String[]> getSplitDataFromFile(@NotNull File file)
|
||
throws IOException {
|
||
ArrayList<String[]> splitDataList = new ArrayList<>();
|
||
|
||
try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
|
||
String line;
|
||
|
||
while ((line = bufferedReader.readLine()) != null) {
|
||
if (line.isEmpty()) {
|
||
continue;
|
||
}
|
||
|
||
String[] splitData = line.split(":");
|
||
splitDataList.add(splitData);
|
||
}
|
||
|
||
} catch (FileNotFoundException e) {
|
||
logger.info("File not found");
|
||
throw e;
|
||
} catch (IOException e) {
|
||
logger.info("IOException reading file");
|
||
throw e;
|
||
}
|
||
|
||
return splitDataList;
|
||
}
|
||
|
||
private @NotNull PlayerProfile addPlayerProfileWithLevelsAndSave(
|
||
FlatFileDatabaseManager databaseManager,
|
||
String playerName,
|
||
UUID uuid,
|
||
int levels) {
|
||
|
||
// Given – DB should not already contain this profile
|
||
assertFalse(databaseManager.loadPlayerProfile(uuid).isLoaded());
|
||
|
||
// When – create new user and level them
|
||
databaseManager.newUser(playerName, uuid);
|
||
PlayerProfile leveledProfile = databaseManager.loadPlayerProfile(uuid);
|
||
|
||
assertTrue(leveledProfile.isLoaded());
|
||
assertEquals(playerName, leveledProfile.getPlayerName());
|
||
assertEquals(uuid, leveledProfile.getUniqueId());
|
||
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
continue;
|
||
}
|
||
|
||
// Note: this also resets XP
|
||
leveledProfile.modifySkill(primarySkillType, levels);
|
||
}
|
||
|
||
databaseManager.saveUser(leveledProfile);
|
||
leveledProfile = databaseManager.loadPlayerProfile(uuid);
|
||
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
continue;
|
||
}
|
||
|
||
assertEquals(levels, leveledProfile.getSkillLevel(primarySkillType));
|
||
}
|
||
|
||
return leveledProfile;
|
||
}
|
||
|
||
private void replaceDataInFile(@NotNull FlatFileDatabaseManager databaseManager,
|
||
@NotNull String[] dataEntries) throws IOException {
|
||
String filePath = databaseManager.getUsersFile().getAbsolutePath();
|
||
|
||
// Given / When – overwrite file contents with provided entries
|
||
try (FileWriter out = new FileWriter(filePath)) {
|
||
StringBuilder writer = new StringBuilder();
|
||
for (String data : dataEntries) {
|
||
writer.append(data).append("\r\n");
|
||
}
|
||
out.write(writer.toString());
|
||
}
|
||
|
||
// Then – log resulting contents for debug visibility
|
||
try (BufferedReader in = new BufferedReader(new FileReader(filePath))) {
|
||
logger.info("Added the following lines to the FlatFileDatabase for the purposes of the test...");
|
||
String line;
|
||
while ((line = in.readLine()) != null) {
|
||
logger.info(line);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void overwriteDataAndCheckForFlag(@NotNull FlatFileDatabaseManager targetDatabase,
|
||
@NotNull String[] data,
|
||
@NotNull FlatFileDataFlag expectedFlag) throws IOException {
|
||
// Given
|
||
replaceDataInFile(targetDatabase, data);
|
||
|
||
// When
|
||
List<FlatFileDataFlag> dataFlags = targetDatabase.checkFileHealthAndStructure();
|
||
|
||
// Then
|
||
assertNotNull(dataFlags);
|
||
assertTrue(dataFlags.contains(expectedFlag));
|
||
}
|
||
|
||
@NotNull
|
||
private Player initMockPlayer(@NotNull String name, @NotNull UUID uuid) {
|
||
Player mockPlayer = mock(Player.class);
|
||
Mockito.when(mockPlayer.getName()).thenReturn(name);
|
||
Mockito.when(mockPlayer.getUniqueId()).thenReturn(uuid);
|
||
return mockPlayer;
|
||
}
|
||
|
||
private static class DebugFilter implements Filter {
|
||
@Override
|
||
public boolean isLoggable(LogRecord record) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
public static void recursiveDelete(@NotNull File directoryToBeDeleted) {
|
||
if (directoryToBeDeleted.isDirectory()) {
|
||
for (File file : directoryToBeDeleted.listFiles()) {
|
||
recursiveDelete(file);
|
||
}
|
||
}
|
||
directoryToBeDeleted.delete();
|
||
}
|
||
|
||
private FlatFileDatabaseManager createDatabaseWithTwoRankedUsers() {
|
||
// Given – a fresh FlatFile DB
|
||
var databaseManager = new FlatFileDatabaseManager(
|
||
new File(getTemporaryUserFilePath()), logger, PURGE_TIME, 0, true);
|
||
|
||
// Given – two users with different levels
|
||
UUID leaderUuid = randomUUID();
|
||
UUID followerUuid = randomUUID();
|
||
|
||
databaseManager.newUser("leader", leaderUuid);
|
||
databaseManager.newUser("follower", followerUuid);
|
||
|
||
var leaderProfile = databaseManager.loadPlayerProfile(leaderUuid);
|
||
var followerProfile = databaseManager.loadPlayerProfile(followerUuid);
|
||
|
||
// Given – leader has higher levels in all non-child skills
|
||
for (PrimarySkillType primarySkillType : PrimarySkillType.values()) {
|
||
if (isChildSkill(primarySkillType)) {
|
||
continue;
|
||
}
|
||
leaderProfile.modifySkill(primarySkillType, 100);
|
||
followerProfile.modifySkill(primarySkillType, 10);
|
||
}
|
||
|
||
// When – save changes back to disk
|
||
databaseManager.saveUser(leaderProfile);
|
||
databaseManager.saveUser(followerProfile);
|
||
|
||
return databaseManager;
|
||
}
|
||
|
||
}
|