/* * 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.component.Component; import com.djrapitops.plan.component.ComponentService; import com.djrapitops.plan.extension.CallEvents; import com.djrapitops.plan.extension.DataExtension; import com.djrapitops.plan.extension.ExtensionSvc; import com.djrapitops.plan.extension.NotReadyException; import com.djrapitops.plan.extension.annotation.*; import com.djrapitops.plan.extension.builder.ExtensionDataBuilder; import com.djrapitops.plan.extension.icon.Color; import com.djrapitops.plan.extension.icon.Icon; import com.djrapitops.plan.extension.implementation.results.*; import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionPlayerDataQuery; import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerDataQuery; import com.djrapitops.plan.extension.implementation.storage.queries.ExtensionServerTableDataQuery; import com.djrapitops.plan.extension.table.Table; import com.djrapitops.plan.gathering.domain.ActiveSession; import com.djrapitops.plan.gathering.domain.WorldTimes; import com.djrapitops.plan.identification.ServerUUID; import com.djrapitops.plan.storage.database.DatabaseTestPreparer; import com.djrapitops.plan.storage.database.transactions.commands.RemoveEverythingTransaction; import com.djrapitops.plan.storage.database.transactions.events.PlayerRegisterTransaction; import com.djrapitops.plan.storage.database.transactions.events.StoreSessionTransaction; import com.djrapitops.plan.storage.database.transactions.events.StoreWorldNameTransaction; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import utilities.OptionalAssert; import utilities.RandomData; import utilities.TestConstants; import utilities.TestErrorLogger; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; /** * Contains database tests for DataExtension API. * * @author AuroraLS3 */ public interface ExtensionsDatabaseTest extends DatabaseTestPreparer { @BeforeEach default void unregisterExtensions() { componentService().register(); ExtensionSvc extensionService = extensionService(); extensionService.register(); extensionService.unregister(new PlayerExtension()); extensionService.unregister(new ServerExtension()); extensionService.unregister(new ConditionalExtension()); extensionService.unregister(new TableExtension()); extensionService.unregister(new ThrowingExtension()); } @Test default void removeEverythingRemovesPlayerExtensionData() { extensionPlayerValuesAreStored(); db().executeTransaction(new RemoveEverythingTransaction()); assertTrue(db().query(new ExtensionPlayerDataQuery(playerUUID)).isEmpty()); } @Test default void removeEverythingRemovesServerExtensionData() { extensionServerValuesAreStored(); db().executeTransaction(new RemoveEverythingTransaction()); assertTrue(db().query(new ExtensionServerDataQuery(serverUUID())).isEmpty()); } @Test default void extensionPlayerValuesAreStored() { db().executeTransaction(new PlayerRegisterTransaction(TestConstants.PLAYER_ONE_UUID, System::currentTimeMillis, TestConstants.PLAYER_ONE_NAME)); ExtensionSvc extensionService = extensionService(); extensionService.register(new PlayerExtension()); extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); Map> playerDataByServerUUID = db().query(new ExtensionPlayerDataQuery(playerUUID)); List ofServer = playerDataByServerUUID.get(serverUUID()); assertNotNull(ofServer); assertFalse(ofServer.isEmpty()); ExtensionData extensionPlayerData = ofServer.get(0); List tabs = extensionPlayerData.getTabs(); assertEquals(1, tabs.size()); // No tab defined, should contain 1 tab ExtensionTabData tabData = tabs.get(0); OptionalAssert.equals("5", tabData.getNumber("value").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("No", tabData.getBoolean("boolVal").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("0.5", tabData.getDouble("doubleVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("0.5", tabData.getPercentage("percentageVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("Something", tabData.getString("stringVal").map(ExtensionStringData::getFormattedValue)); OptionalAssert.equals("Group", tabData.getString("groupVal").map(ExtensionStringData::getFormattedValue)); ObjectMapper objectMapper = new ObjectMapper(); ObjectNode objectNode = objectMapper.createObjectNode(); objectNode.put("color", "green"); objectNode.put("text", "Test"); OptionalAssert.equals(objectNode, tabData.getComponent("componentVal").map(ExtensionComponentData::getFormattedValue).map(str -> { try { return objectMapper.readTree(str); } catch (Throwable t) { return fail(t); } })); } @Test default void extensionPlayerValuesCanBeQueriedAsTableData() { extensionPlayerValuesAreStored(); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[0])); db().executeTransaction(new StoreWorldNameTransaction(serverUUID(), worlds[1])); // Store a session to check against issue https://github.com/plan-player-analytics/Plan/issues/1039 ActiveSession session = new ActiveSession(playerUUID, serverUUID(), 32345L, worlds[0], "SURVIVAL"); session.getExtraData().put(WorldTimes.class, RandomData.randomWorldTimes(worlds)); db().executeTransaction(new StoreSessionTransaction(session.toFinishedSession(42345L))); Map result = db().query(new ExtensionServerTableDataQuery(serverUUID(), 50)); assertEquals(1, result.size()); ExtensionTabData playerData = result.get(playerUUID); assertNotNull(playerData); OptionalAssert.equals("5", playerData.getNumber("value").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("No", playerData.getBoolean("boolVal").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("0.5", playerData.getDouble("doubleVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("0.5", playerData.getPercentage("percentageVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("Something", playerData.getString("stringVal").map(ExtensionStringData::getFormattedValue)); } @Test default void extensionServerValuesAreStored() { ExtensionSvc extensionService = extensionService(); extensionService.register(new ServerExtension()); extensionService.updateServerValues(CallEvents.SERVER_EXTENSION_REGISTER); List ofServer = db().query(new ExtensionServerDataQuery(serverUUID())); assertFalse(ofServer.isEmpty()); ExtensionData extensionData = ofServer.get(0); List tabs = extensionData.getTabs(); assertEquals(1, tabs.size()); // No tab defined, should contain 1 tab ExtensionTabData tabData = tabs.get(0); OptionalAssert.equals("5", tabData.getNumber("value").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("No", tabData.getBoolean("boolVal").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("0.5", tabData.getDouble("doubleVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("0.5", tabData.getPercentage("percentageVal").map(data -> data.getFormattedValue(Object::toString))); OptionalAssert.equals("Something", tabData.getString("stringVal").map(ExtensionStringData::getFormattedValue)); } @Test default void extensionServerAggregateQueriesWork() { ExtensionSvc extensionService = extensionService(); extensionService.register(new PlayerExtension()); extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); List ofServer = db().query(new ExtensionServerDataQuery(serverUUID())); assertFalse(ofServer.isEmpty()); ExtensionData extensionData = ofServer.get(0); List tabs = extensionData.getTabs(); assertEquals(1, tabs.size()); // No tab defined, should contain 1 tab ExtensionTabData tabData = tabs.get(0); OptionalAssert.equals("0.0", tabData.getPercentage("boolVal_aggregate").map(data -> data.getFormattedValue(Objects::toString))); OptionalAssert.equals("0.5", tabData.getPercentage("percentageVal_avg").map(data -> data.getFormattedValue(Objects::toString))); OptionalAssert.equals("0.5", tabData.getDouble("doubleVal_avg").map(data -> data.getFormattedValue(Objects::toString))); OptionalAssert.equals("0.5", tabData.getDouble("doubleVal_total").map(data -> data.getFormattedValue(Objects::toString))); OptionalAssert.equals("5", tabData.getNumber("value_avg").map(data -> data.getFormattedValue(Objects::toString))); OptionalAssert.equals("5", tabData.getNumber("value_total").map(data -> data.getFormattedValue(Objects::toString))); List tableData = tabData.getTableData(); assertEquals(1, tableData.size()); Table expected = Table.builder() .columnOne("a group", Icon.called("circle").build()) .columnTwo("Players", Icon.called("user").build()) .addRow("Group", 1).build(); Table result = tableData.get(0).getTable(); assertEquals(expected, result); } @Test default void unsatisfiedPlayerConditionalResultsAreCleaned() { db().executeTransaction(new PlayerRegisterTransaction(playerUUID, System::currentTimeMillis, TestConstants.PLAYER_ONE_NAME)); ExtensionSvc extensionService = extensionService(); extensionService.register(new ConditionalExtension()); ConditionalExtension.condition = true; extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); // Check that the wanted data exists checkThatPlayerDataExists(ConditionalExtension.condition); // Reverse condition ConditionalExtension.condition = false; extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); // Check that the wanted data exists checkThatPlayerDataExists(ConditionalExtension.condition); // Reverse condition ConditionalExtension.condition = false; extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); // Check that the wanted data exists checkThatPlayerDataExists(ConditionalExtension.condition); } default void checkThatPlayerDataExists(boolean condition) { if (condition) { // Condition is true, conditional values exist List ofServer = db().query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID()); assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left"); ExtensionTabData tabData = ofServer.get(0).getTabs().get(0); OptionalAssert.equals("Yes", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("Conditional", tabData.getString("conditionalValue").map(ExtensionStringData::getFormattedValue)); OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed OptionalAssert.equals("Group", tabData.getString("conditionalGroups").map(ExtensionStringData::getFormattedValue)); // Was not removed assertFalse(tabData.getString("reversedConditionalValue").isPresent(), "Value was not removed: reversedConditionalValue"); } else { // Condition is false, reversed conditional values exist List ofServer = db().query(new ExtensionPlayerDataQuery(playerUUID)).get(serverUUID()); assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left"); ExtensionTabData tabData = ofServer.get(0).getTabs().get(0); OptionalAssert.equals("No", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("Reversed", tabData.getString("reversedConditionalValue").map(ExtensionStringData::getFormattedValue)); OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed assertFalse(tabData.getString("conditionalValue").isPresent(), "Value was not removed: conditionalValue"); assertFalse(tabData.getString("conditionalGroups").isPresent(), "Value was not removed: conditionalGroups"); } } @Test default void unsatisfiedServerConditionalResultsAreCleaned() { ExtensionSvc extensionService = extensionService(); ConditionalExtension.condition = true; extensionService.register(new ConditionalExtension()); extensionService.updateServerValues(CallEvents.MANUAL); // Check that the wanted data exists checkThatServerDataExists(ConditionalExtension.condition); // Reverse condition ConditionalExtension.condition = false; extensionService.updateServerValues(CallEvents.MANUAL); // Check that the wanted data exists checkThatServerDataExists(ConditionalExtension.condition); // Reverse condition ConditionalExtension.condition = false; extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); // Check that the wanted data exists checkThatServerDataExists(ConditionalExtension.condition); } default void checkThatServerDataExists(boolean condition) { if (condition) { // Condition is true, conditional values exist List ofServer = db().query(new ExtensionServerDataQuery(serverUUID())); assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left"); ExtensionTabData tabData = ofServer.get(0).getTabs().get(0); OptionalAssert.equals("Yes", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("Conditional", tabData.getString("conditionalValue").map(ExtensionStringData::getFormattedValue)); OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed assertFalse(tabData.getString("reversedConditionalValue").isPresent(), "Value was not removed: reversedConditionalValue"); } else { // Condition is false, reversed conditional values exist List ofServer = db().query(new ExtensionServerDataQuery(serverUUID())); assertTrue(ofServer != null && !ofServer.isEmpty() && !ofServer.get(0).getTabs().isEmpty(), "There was no data left"); ExtensionTabData tabData = ofServer.get(0).getTabs().get(0); OptionalAssert.equals("No", tabData.getBoolean("isCondition").map(ExtensionBooleanData::getFormattedValue)); OptionalAssert.equals("Reversed", tabData.getString("reversedConditionalValue").map(ExtensionStringData::getFormattedValue)); OptionalAssert.equals("unconditional", tabData.getString("unconditional").map(ExtensionStringData::getFormattedValue)); // Was not removed assertFalse(tabData.getString("conditionalValue").isPresent(), "Value was not removed: conditionalValue"); } } @Test default void extensionServerTableValuesAreInserted() { ExtensionSvc extensionService = extensionService(); extensionService.register(new TableExtension()); extensionService.updateServerValues(CallEvents.MANUAL); extensionService.updateServerValues(CallEvents.MANUAL); List ofServer = db().query(new ExtensionServerDataQuery(serverUUID())); assertFalse(ofServer.isEmpty()); ExtensionData extensionData = ofServer.get(0); List tabs = extensionData.getTabs(); assertEquals(1, tabs.size()); // No tab defined, should contain 1 tab ExtensionTabData tabData = tabs.get(0); List tableData = tabData.getTableData(); assertEquals(1, tableData.size()); ExtensionTableData table = tableData.get(0); Table expected = Table.builder() .columnOne("first", Icon.called("gavel").build()) .columnTwo("second", Icon.called("what").build()) .columnThree("third", Icon.called("question").build()) .columnFive("five", Icon.called("").build()) .addRow("value", 3, 0.5) .build(); assertEquals(expected, table.getTable()); } @Test default void extensionPlayerTableValuesAreInserted() { ExtensionSvc extensionService = extensionService(); extensionService.register(new TableExtension()); extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); Map> ofPlayer = db().query(new ExtensionPlayerDataQuery(playerUUID)); assertFalse(ofPlayer.isEmpty()); List ofServer = ofPlayer.get(serverUUID()); assertEquals(1, ofServer.size()); ExtensionData extensionServerData = ofServer.get(0); List tabs = extensionServerData.getTabs(); assertEquals(1, tabs.size()); // No tab defined, should contain 1 tab ExtensionTabData tabData = tabs.get(0); List tableData = tabData.getTableData(); assertEquals(1, tableData.size()); ExtensionTableData table = tableData.get(0); Table expected = Table.builder() .columnOne("first", Icon.called("gavel").build()) .columnTwo("second", Icon.called("what").build()) .columnThree("third", Icon.called("question").build()) .addRow("value", 3, 0.5) .build(); assertEquals(expected, table.getTable()); } @Test @Disabled("Flaky test, possibly due to some kind of concurrent execution - one extra exception is sometimes caught") default void extensionExceptionsAreCaught() { TestErrorLogger.throwErrors(false); ExtensionSvc extensionService = extensionService(); extensionService.register(new ThrowingExtension()); extensionService.updateServerValues(CallEvents.MANUAL); extensionService.updatePlayerValues(playerUUID, TestConstants.PLAYER_ONE_NAME, CallEvents.MANUAL); // 5 of the exceptions need to be logged, there are 8 exceptions total 3 of which are ignored. assertEquals(5, TestErrorLogger.getCaught().size(), () -> "Not all exceptions got logged, logged exceptions: " + TestErrorLogger.getCaught().toString()); } @PluginInfo(name = "ConditionalExtension") class ConditionalExtension implements DataExtension { static boolean condition = true; @BooleanProvider(text = "a boolean", conditionName = "condition") public boolean isCondition(UUID playerUUID) { return condition; } @StringProvider(text = "Conditional Value") @Conditional("condition") public String conditionalValue(UUID playerUUID) { return "Conditional"; } @StringProvider(text = "Reversed Conditional Value") @Conditional(value = "condition", negated = true) public String reversedConditionalValue(UUID playerUUID) { return "Reversed"; } @StringProvider(text = "Unconditional") public String unconditional(UUID playerUUID) { return "unconditional"; } @BooleanProvider(text = "a boolean", conditionName = "condition") public boolean isCondition() { return condition; } @StringProvider(text = "Conditional Value") @Conditional("condition") public String conditionalValue() { return "Conditional"; } @StringProvider(text = "Reversed Conditional Value") @Conditional(value = "condition", negated = true) public String reversedConditionalValue() { return "Reversed"; } @GroupProvider(text = "Conditional Group") @Conditional("condition") public String[] conditionalGroups(UUID playerUUID) { return new String[]{"Group"}; } @StringProvider(text = "Unconditional") public String unconditional() { return "unconditional"; } } @PluginInfo(name = "ServerExtension") class ServerExtension implements DataExtension { @NumberProvider(text = "a number") public long value() { return 5L; } @BooleanProvider(text = "a boolean") public boolean boolVal() { return false; } @DoubleProvider(text = "a double") public double doubleVal() { return 0.5; } @PercentageProvider(text = "a percentage") public double percentageVal() { return 0.5; } @StringProvider(text = "a string") public String stringVal() { return "Something"; } } @PluginInfo(name = "PlayerExtension") class PlayerExtension implements DataExtension { @NumberProvider(text = "a number", showInPlayerTable = true) public long value(UUID playerUUD) { return 5L; } @BooleanProvider(text = "a boolean", showInPlayerTable = true) public boolean boolVal(UUID playerUUID) { return false; } @DoubleProvider(text = "a double", showInPlayerTable = true) public double doubleVal(UUID playerUUID) { return 0.5; } @PercentageProvider(text = "a percentage", showInPlayerTable = true) public double percentageVal(UUID playerUUID) { return 0.5; } @StringProvider(text = "a string", showInPlayerTable = true) public String stringVal(UUID playerUUID) { return "Something"; } @GroupProvider(text = "a group") public String[] groupVal(UUID playerUUID) { return new String[]{"Group"}; } @ComponentProvider(text = "colored text") public Component componentVal(UUID playerUUID) { return ComponentService.getInstance().fromLegacy("&aTest", '&'); } } @PluginInfo(name = "TableExtension") class TableExtension implements DataExtension { @TableProvider(tableColor = Color.AMBER) public Table table() { return createTestTable(); } @TableProvider(tableColor = Color.AMBER) public Table playerTable(UUID playerUUID) { return createTestTable(); } private Table createTestTable() { return Table.builder() .columnOne("first", Icon.called("gavel").of(Color.AMBER).build()) .columnTwo("second", Icon.called("what").of(Color.BROWN).build()) // Colors are ignored .columnThree("third", null) // Can handle improper icons .columnFive("five", Icon.called("").build()) // Can handle null column in between and ignore the next column .addRow("value", 3, 0.5, 400L) // Can handle too many row values .build(); } } @PluginInfo(name = "ThrowingExtension") class ThrowingExtension implements DataExtension { @BooleanProvider(text = "a boolean") public boolean booleanMethod() { throw new IllegalArgumentException("Failed to catch"); } @BooleanProvider(text = "a boolean") public boolean booleanPlayerMethod(UUID playerUUID) { throw new NotReadyException(); } @StringProvider(text = "a string") public String stringMethod() { throw new NoSuchMethodError(); } @NumberProvider(text = "a string") public long numberMethod() { throw new UnsupportedOperationException(); } @GroupProvider(text = "group") public String[] groupMethod(UUID playerUUID) { throw new NoClassDefFoundError(); } @DataBuilderProvider public ExtensionDataBuilder builder() { return newExtensionDataBuilder() .addValue(String.class, () -> { throw new NotReadyException(); }); } @DataBuilderProvider public ExtensionDataBuilder builder2() { return newExtensionDataBuilder() .addValue(String.class, () -> { throw new NoClassDefFoundError(); }); } @DataBuilderProvider public ExtensionDataBuilder builder3() { throw new NoSuchMethodError(); } } }