/* * This file is part of Player Analytics (Plan). * * Plan is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License v3 as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Plan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Plan. If not, see . */ package com.djrapitops.plan.storage.database.queries; import com.djrapitops.plan.delivery.domain.TablePlayer; import com.djrapitops.plan.delivery.domain.container.PlayerContainer; import com.djrapitops.plan.delivery.domain.mutators.SessionsMutator; import com.djrapitops.plan.gathering.domain.*; import com.djrapitops.plan.identification.Server; import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.storage.database.DatabaseTestPreparer; import com.djrapitops.plan.storage.database.queries.containers.PlayerContainerQuery; import com.djrapitops.plan.storage.database.queries.objects.BaseUserQueries; import com.djrapitops.plan.storage.database.queries.objects.KillQueries; import com.djrapitops.plan.storage.database.queries.objects.SessionQueries; import com.djrapitops.plan.storage.database.queries.objects.WorldTimesQueries; import com.djrapitops.plan.storage.database.queries.objects.playertable.ServerTablePlayersQuery; import com.djrapitops.plan.storage.database.sql.tables.WorldTable; import com.djrapitops.plan.storage.database.transactions.ExecStatement; import com.djrapitops.plan.storage.database.transactions.StoreServerInformationTransaction; import com.djrapitops.plan.storage.database.transactions.Transaction; import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction; import com.djrapitops.plan.storage.database.transactions.events.*; import com.djrapitops.plan.utilities.java.Maps; import net.playeranalytics.plugin.scheduling.TimeAmount; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import utilities.RandomData; import utilities.TestConstants; import java.sql.PreparedStatement; import java.util.*; import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.*; public interface SessionQueriesTest extends DatabaseTestPreparer { @Test default void sessionStoreTransactionOutOfOrderDoesNotFailDueToMissingMainUser() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(player2UUID, RandomData::randomTime, TestConstants.PLAYER_TWO_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); assertEquals(session, savedSessions.get(0)); } @Test default void sessionStoreTransactionOutOfOrderDoesNotFailDueToMissingKilledUser() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(playerUUID, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); // Query doesn't fetch kills since the uuid for killed player is missing session.getExtraData(PlayerKills.class) .map(PlayerKills::asList) .ifPresent(List::clear); assertEquals(session, savedSessions.get(0)); } @Test default void shutdownDataPreservationTransactionOutOfOrderDoesNotFailDueToMissingMainUser() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(player2UUID, RandomData::randomTime, TestConstants.PLAYER_TWO_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new ShutdownDataPreservationTransaction(List.of(session))); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); assertEquals(session, savedSessions.get(0)); } @Test default void shutdownDataPreservationTransactionOutOfOrderDoesNotFailDueToMissingKilledUser() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(playerUUID, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new ShutdownDataPreservationTransaction(List.of(session))); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); assertEquals(session, savedSessions.get(0)); } @Test default void killsAreAvailableAfter2ndUserRegisterEvenIfOutOfOrder() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(playerUUID, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); // killed user is registered after session already ended. db().executeTransaction(new StoreServerPlayerTransaction(player2UUID, RandomData::randomTime, TestConstants.PLAYER_TWO_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); assertEquals(session, savedSessions.get(0)); } @Test default void sessionStoreTransactionOutOfOrderUpdatesUserInformation() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); long registerDate = RandomData.randomTime(); db().executeTransaction(new PlayerRegisterTransaction(playerUUID, () -> registerDate, TestConstants.PLAYER_ONE_NAME)); Optional expected = Optional.of(new BaseUser(playerUUID, TestConstants.PLAYER_ONE_NAME, registerDate, 0)); Optional result = db().query(BaseUserQueries.fetchBaseUserOfPlayer(playerUUID)); assertEquals(expected, result); } @Test default void sessionPlaytimeIsCalculatedCorrectlyAfterStorage() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); long expectedLength = session.getLength(); long sessionEnd = session.getEnd(); db().executeTransaction(new StoreSessionTransaction(session)); forcePersistenceCheck(); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); assertTrue(sessions.containsKey(serverUUID())); SessionsMutator sessionsMutator = new SessionsMutator(sessions.get(serverUUID())); assertEquals(expectedLength, sessionsMutator.toPlaytime()); assertEquals(1, sessionsMutator.count()); SessionsMutator afterTimeSessionsMutator = sessionsMutator.filterSessionsBetween(sessionEnd + 1L, System.currentTimeMillis()); assertEquals(0L, afterTimeSessionsMutator.toPlaytime()); assertEquals(0, afterTimeSessionsMutator.count()); } default void prepareForSessionSave() { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); db().executeTransaction(new StoreServerPlayerTransaction(playerUUID, RandomData::randomTime, TestConstants.PLAYER_ONE_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); db().executeTransaction(new StoreServerPlayerTransaction(player2UUID, RandomData::randomTime, TestConstants.PLAYER_TWO_NAME, serverUUID(), TestConstants.GET_PLAYER_HOSTNAME)); } @Test default void sessionsAreStoredWithAllData() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); forcePersistenceCheck(); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertEquals(1, savedSessions.size()); assertEquals(session, savedSessions.get(0)); } @Test default void mostRecentSessionsCanBeQueried() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); List expected = Collections.singletonList(session); List result = db().query(SessionQueries.fetchLatestSessionsOfServer(serverUUID(), 1)); assertEquals(expected, result); } @Test default void worldTimesAreSavedWithAllSessionSave() { prepareForSessionSave(); WorldTimes worldTimes = RandomData.randomWorldTimes(worlds); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID); session.getExtraData().put(WorldTimes.class, worldTimes); List sessions = Collections.singletonList(session); db().executeTransaction(new Transaction() { @Override protected void performOperations() { execute(LargeStoreQueries.storeAllSessionsWithKillAndWorldData(sessions)); } }); Map saved = db().query(WorldTimesQueries.fetchPlayerWorldTimesOnServers(playerUUID)); WorldTimes savedWorldTimes = saved.get(serverUUID()); assertEquals(worldTimes, savedWorldTimes); } @Test default void worldTimesAreSavedWithSession() { prepareForSessionSave(); WorldTimes worldTimes = RandomData.randomWorldTimes(worlds); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID); session.getExtraData().put(WorldTimes.class, worldTimes); List sessions = new ArrayList<>(); sessions.add(session); db().executeTransaction(new Transaction() { @Override protected void performOperations() { execute(LargeStoreQueries.storeAllSessionsWithKillAndWorldData(sessions)); } }); List allSessions = db().query(SessionQueries.fetchAllSessions()); assertEquals(worldTimes, allSessions.get(0).getExtraData(WorldTimes.class) .orElseThrow(AssertionError::new)); } @Test default void worldTimesAreSavedWithSessionWithoutWorlds() { prepareForSessionSave(); // Remove the worlds from the database so that they need to also be saved. execute(new ExecStatement("DELETE FROM " + WorldTable.TABLE_NAME) { @Override public void prepare(PreparedStatement statement) { // Nothing needed } }); WorldTimes worldTimes = RandomData.randomWorldTimes(worlds); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID); session.getExtraData().put(WorldTimes.class, worldTimes); List sessions = new ArrayList<>(); sessions.add(session); db().executeTransaction(new Transaction() { @Override protected void performOperations() { execute(LargeStoreQueries.storeAllSessionsWithKillAndWorldData(sessions)); } }); List allSessions = db().query(SessionQueries.fetchAllSessions()); assertEquals(worldTimes, allSessions.get(0).getExtraData(WorldTimes.class) .orElseThrow(AssertionError::new)); } @Test default void playersWorldTimesMatchTotal() { worldTimesAreSavedWithSession(); FinishedSession session = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)).get(serverUUID()).get(0); WorldTimes expected = session.getExtraData(WorldTimes.class).orElseThrow(AssertionError::new); WorldTimes worldTimesOfUser = db().query(WorldTimesQueries.fetchPlayerTotalWorldTimes(playerUUID)); assertEquals(expected, worldTimesOfUser); } @Test default void serverWorldTimesMatchTotal() { worldTimesAreSavedWithSession(); FinishedSession session = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)).get(serverUUID()).get(0); WorldTimes expected = session.getExtraData(WorldTimes.class).orElseThrow(AssertionError::new); WorldTimes worldTimesOfServer = db().query(WorldTimesQueries.fetchServerTotalWorldTimes(serverUUID())); assertEquals(expected, worldTimesOfServer); } @Test default void emptyServerWorldTimesIsEmpty() { WorldTimes worldTimesOfServer = db().query(WorldTimesQueries.fetchServerTotalWorldTimes(serverUUID())); assertEquals(new WorldTimes(), worldTimesOfServer); } @Test default void playerSessionsAreFetchedByServerUUID() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); db().executeTransaction(new StoreSessionTransaction(session)); forcePersistenceCheck(); Map> expected = Collections.singletonMap(serverUUID(), Collections.singletonList(session)); Map> fetched = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); assertEquals(expected, fetched); } @Test default void testKillTableGetKillsOfServer() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); List expected = session.getExtraData(PlayerKills.class).map(PlayerKills::asList) .orElseThrow(AssertionError::new); db().executeTransaction(new StoreSessionTransaction(session)); forcePersistenceCheck(); Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List savedSessions = sessions.get(serverUUID()); assertNotNull(savedSessions); assertFalse(savedSessions.isEmpty()); List got = savedSessions.get(0).getExtraData(PlayerKills.class).map(PlayerKills::asList) .orElseThrow(AssertionError::new); assertEquals(expected, got); } @Test default void sessionWorldTimesAreFetchedCorrectly() { prepareForSessionSave(); FinishedSession session = RandomData.randomSession(serverUUID(), worlds, playerUUID, player2UUID); WorldTimes expected = session.getExtraData(WorldTimes.class).orElseThrow(AssertionError::new); db().executeTransaction(new StoreSessionTransaction(session)); // Fetch the session Map> sessions = db().query(SessionQueries.fetchSessionsOfPlayer(playerUUID)); List serverSessions = sessions.get(serverUUID()); assertNotNull(serverSessions); assertFalse(serverSessions.isEmpty()); WorldTimes got = serverSessions.get(0).getExtraData(WorldTimes.class).orElseThrow(AssertionError::new); assertEquals(expected, got); } @Test default void removeEverythingRemovesSessions() { sessionsAreStoredWithAllData(); db().executeTransaction(new RemoveEverythingTransaction()); assertTrue(db().query(SessionQueries.fetchAllSessions()).isEmpty()); assertEquals(0L, db().query(WorldTimesQueries.fetchServerTotalWorldTimes(serverUUID())).getTotal()); assertEquals(0, db().query(KillQueries.playerKillCount(0, System.currentTimeMillis(), serverUUID()))); } @Test default void removeEverythingRemovesWorldNames() { prepareForSessionSave(); db().executeTransaction(new RemoveEverythingTransaction()); assertTrue(db().query(LargeFetchQueries.fetchAllWorldNames()).isEmpty()); } @Test default void worldNamesAreStored() { String[] expected = {"Test", "Test2", "Test3"}; for (String worldName : expected) { db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worldName)); } forcePersistenceCheck(); Collection result = db().query(LargeFetchQueries.fetchAllWorldNames()).get(serverUUID()); assertEquals(new HashSet<>(Arrays.asList(expected)), result); } @RepeatedTest(value = 3, name = "Players table and Player page playtimes match {currentRepetition}/{totalRepetitions}") default void playersTableAndPlayerPagePlaytimeMatches() { prepareForSessionSave(); List player1Sessions = RandomData.randomSessions(serverUUID(), worlds, playerUUID, player2UUID); List player2Sessions = RandomData.randomSessions(serverUUID(), worlds, player2UUID, playerUUID); player1Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); player2Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); long playtimeThreshold = RandomData.randomLong(TimeUnit.HOURS.toMillis(1L), TimeUnit.DAYS.toMillis(2L)); PlayerContainer playerContainer = db().query(new PlayerContainerQuery(playerUUID)); TablePlayer tablePlayer = db().query(new ServerTablePlayersQuery(serverUUID(), System.currentTimeMillis(), playtimeThreshold, 5)) .stream().filter(player -> playerUUID.equals(player.getPlayerUUID())).findAny() .orElseThrow(AssertionError::new); long expected = SessionsMutator.forContainer(playerContainer).toActivePlaytime(); long got = tablePlayer.getActivePlaytime().orElseThrow(AssertionError::new); assertEquals(expected, got); } @RepeatedTest(value = 3, name = "Players table and player page Activity Index calculations match {currentRepetition}/{totalRepetitions}") default void playersTableAndPlayerPageActivityIndexMatches() { prepareForSessionSave(); List player1Sessions = RandomData.randomSessions(serverUUID(), worlds, playerUUID, player2UUID); List player2Sessions = RandomData.randomSessions(serverUUID(), worlds, player2UUID, playerUUID); player1Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); player2Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); long time = System.currentTimeMillis(); long playtimeThreshold = RandomData.randomLong(TimeUnit.HOURS.toMillis(1L), TimeUnit.DAYS.toMillis(2L)); PlayerContainer playerContainer = db().query(new PlayerContainerQuery(playerUUID)); TablePlayer tablePlayer = db().query(new ServerTablePlayersQuery(serverUUID(), time, playtimeThreshold, 5)) .stream().filter(player -> playerUUID.equals(player.getPlayerUUID())).findAny() .orElseThrow(AssertionError::new); SessionsMutator sessionsMutator = SessionsMutator.forContainer(playerContainer); long week = TimeAmount.WEEK.toMillis(1L); long weekAgo = time - week; long twoWeeksAgo = time - 2L * week; long threeWeeksAgo = time - 3L * week; SessionsMutator weekOne = sessionsMutator.filterSessionsBetween(weekAgo, time); SessionsMutator weekTwo = sessionsMutator.filterSessionsBetween(twoWeeksAgo, weekAgo); SessionsMutator weekThree = sessionsMutator.filterSessionsBetween(threeWeeksAgo, twoWeeksAgo); long playtime1 = weekOne.toActivePlaytime(); long playtime2 = weekTwo.toActivePlaytime(); long playtime3 = weekThree.toActivePlaytime(); double expected = playerContainer.getActivityIndex(time, playtimeThreshold).getValue(); double got = tablePlayer.getCurrentActivityIndex().orElseThrow(AssertionError::new).getValue(); assertEquals(expected, got, 0.001, () -> "Activity Indexes between queries differed, expected: <" + expected + "> but was: <" + got + ">" + ". Playtime for reference container: " ); } @Test default void serverPreferencePieValuesAreCorrect() { prepareForSessionSave(); List server1Sessions = RandomData.randomSessions(serverUUID(), worlds, playerUUID, player2UUID); server1Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); ServerUUID serverTwoUuid = TestConstants.SERVER_TWO_UUID; executeTransactions(new StoreServerInformationTransaction(new Server(serverTwoUuid, TestConstants.SERVER_TWO_NAME, "", TestConstants.VERSION))); db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverTwoUuid, worlds[1])); List server2Sessions = RandomData.randomSessions(serverTwoUuid, worlds, playerUUID, player2UUID); server2Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); Map expected = Maps.builder(String.class, Long.class) .put(TestConstants.SERVER_NAME, new SessionsMutator(server1Sessions).toPlaytime()) .put(TestConstants.SERVER_TWO_NAME, new SessionsMutator(server2Sessions).toPlaytime()) .build(); Map results = db().query(SessionQueries.playtimePerServer(Long.MIN_VALUE, Long.MAX_VALUE)); assertEquals(expected, results); } @Test @DisplayName("Last seen query by server uuid groups last seen by player") default void lastSeenByServerIsGroupedByPlayer() { prepareForSessionSave(); List player1Sessions = RandomData.randomSessions(serverUUID(), worlds, playerUUID, player2UUID); List player2Sessions = RandomData.randomSessions(serverUUID(), worlds, player2UUID, playerUUID); player1Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); player2Sessions.forEach(session -> db().executeTransaction(new StoreSessionTransaction(session))); long lastSeenP1 = new SessionsMutator(player1Sessions).toLastSeen(); long lastSeenP2 = new SessionsMutator(player2Sessions).toLastSeen(); Map expected = Map.of(playerUUID, lastSeenP1, player2UUID, lastSeenP2); Map result = db().query(SessionQueries.lastSeen(serverUUID())); assertEquals(expected, result); } }