diff --git a/.gitignore b/.gitignore index 4d3b4a331..10b4e76f0 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ !/settings.gradle !/gradle.properties +!/buildSrc/ + !/README.md !/api/ @@ -18,4 +20,4 @@ !/spigot/ !/sponge/ !/sponge8/ -!/velocity/ \ No newline at end of file +!/velocity/ diff --git a/build.gradle b/build.gradle index 2a9fa3fd9..59993c7c5 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,7 @@ subprojects { dependencies { compileOnly 'com.google.code.findbugs:jsr305:3.0.2' compileOnly 'org.checkerframework:checker-qual:3.7.0' + testCompileOnly 'org.checkerframework:checker-qual:3.7.0' } sourceCompatibility = '1.8' diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 000000000..366b32788 --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1,6 @@ +/* + +!/.gitignore + +!/build.gradle +!/src/ diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 000000000..509b5cdc1 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,18 @@ +sourceSets { + main { + java.srcDirs = ['src'] + resources.srcDirs = ['resources'] + } + test { + java.srcDirs = ['testSrc'] + resources.srcDirs = ['testResources'] + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly 'org.checkerframework:checker-qual:3.7.0' +} diff --git a/buildSrc/src/com/elikill58/negativity/build/TestPropertiesCollector.java b/buildSrc/src/com/elikill58/negativity/build/TestPropertiesCollector.java new file mode 100644 index 000000000..2579aec36 --- /dev/null +++ b/buildSrc/src/com/elikill58/negativity/build/TestPropertiesCollector.java @@ -0,0 +1,117 @@ +package com.elikill58.negativity.build; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.gradle.api.tasks.testing.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestPropertiesCollector { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestPropertiesCollector.class); + + // Matches NEGATIVITY_DB_MYSQL_URL + // Where MYSQL is a database config name + // and URL is a config key (URL, USER, PASSWORD or TYPE) + private static final Pattern DB_CONFIG_PATTERN = Pattern.compile("^NEGATIVITY_DB_(?\\p{Upper}+)_(?\\p{Upper}+)$"); + + /** + * Collects environment variables to be used as system properties of a test task. + *
+ * The following properties are supported: + * + * + * @return system properties to pass to a test task + * + * @see Test#systemProperties + * @see #applyTestProperties(Test) + */ + public static Map collectTestProperties() { + Map databaseConfigs = new HashMap<>(); + System.getenv().forEach((envKey, value) -> { + Matcher dbConfigMatcher = DB_CONFIG_PATTERN.matcher(envKey); + if (dbConfigMatcher.matches()) { + String configName = dbConfigMatcher.group("name"); + String configKey = dbConfigMatcher.group("key"); + DatabaseConfig databaseConfig = databaseConfigs.computeIfAbsent(configName, name -> new DatabaseConfig()); + switch (configKey) { + case "URL": + databaseConfig.url = value; + break; + case "USER": + databaseConfig.user = value; + break; + case "PASSWORD": + databaseConfig.password = value; + break; + case "TYPE": + databaseConfig.type = value; + break; + default: + LOGGER.warn("Unknown database configuration key '{}'", configKey); + } + } + }); + + Map systemProperties = new HashMap<>(); + + Set databaseConfigNames = new HashSet<>(); + for (Map.Entry entry : databaseConfigs.entrySet()) { + String key = entry.getKey(); + DatabaseConfig config = entry.getValue(); + if (config.url == null) { + LOGGER.error("Missing url of database configuration '{}'", key); + continue; + } else if (config.user == null) { + LOGGER.error("Missing user of database configuration '{}'", key); + continue; + } else if (config.password == null) { + LOGGER.error("Missing password of database configuration '{}'", key); + continue; + } else if (config.type == null) { + LOGGER.error("Missing password of database configuration '{}'", key); + continue; + } + + databaseConfigNames.add(key); + systemProperties.put("negativity.db." + key + ".url", config.url); + systemProperties.put("negativity.db." + key + ".user", config.user); + systemProperties.put("negativity.db." + key + ".password", config.password); + systemProperties.put("negativity.db." + key + ".type", config.type); + } + + systemProperties.put("negativity.databases", String.join(",", databaseConfigNames)); + return systemProperties; + } + + public static void applyTestProperties(Test testTask) { + Map testProperties = collectTestProperties(); + testTask.getInputs().properties(testProperties); + testTask.systemProperties(testProperties); + } + + private static class DatabaseConfig { + + public @Nullable String url; + public @Nullable String user; + public @Nullable String password; + public @Nullable String type; + } +} diff --git a/common/build.gradle b/common/build.gradle index b1e6d593f..60241f18f 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -1,3 +1,4 @@ +import com.elikill58.negativity.build.TestPropertiesCollector import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { @@ -18,6 +19,7 @@ sourceSets { dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0") + testRuntimeOnly("mysql:mysql-connector-java:8.0.25") } processResources { @@ -51,4 +53,16 @@ parent.tasks.named('shadowJar', ShadowJar) { test { useJUnitPlatform() + + workingDir(file('testRun')) + doFirst { + workingDir.mkdirs() + } + + systemProperty 'negativity.testing', 'true' + TestPropertiesCollector.applyTestProperties(test) +} + +tasks.named('cleanTest', Delete.class) { + it.delete(test.workingDir) } diff --git a/common/src/com/elikill58/negativity/universal/Adapter.java b/common/src/com/elikill58/negativity/universal/Adapter.java index 049901ee6..134b6ed7a 100644 --- a/common/src/com/elikill58/negativity/universal/Adapter.java +++ b/common/src/com/elikill58/negativity/universal/Adapter.java @@ -26,7 +26,7 @@ public abstract class Adapter { private static Adapter adapter = null; public static void setAdapter(Adapter adapter) { - if(Adapter.adapter != null) { + if(Adapter.adapter != null && !Boolean.getBoolean("negativity.testing")) { try { throw new IllegalAccessException("No ! You don't must to change the Adapter !"); } catch (IllegalAccessException e) { diff --git a/common/src/com/elikill58/negativity/universal/Database.java b/common/src/com/elikill58/negativity/universal/Database.java index 9b90280b6..754aded1f 100644 --- a/common/src/com/elikill58/negativity/universal/Database.java +++ b/common/src/com/elikill58/negativity/universal/Database.java @@ -96,6 +96,10 @@ public static void init() { } } + public static DatabaseType getType() { + return databaseType; + } + public static enum DatabaseType { MARIA("mariadb", "MariaDB", "org.mariadb.jdbc.Driver"), MYSQL("mysql", "MySQL", "com.mysql.jdbc.Driver"); diff --git a/common/src/com/elikill58/negativity/universal/Platform.java b/common/src/com/elikill58/negativity/universal/Platform.java index 857b63727..d8c3c7cc4 100644 --- a/common/src/com/elikill58/negativity/universal/Platform.java +++ b/common/src/com/elikill58/negativity/universal/Platform.java @@ -5,7 +5,8 @@ public enum Platform { BUNGEE("bungee", true), SPIGOT("spigot", false), SPONGE("sponge", false), - VELOCITY("velocity", true); + VELOCITY("velocity", true), + TEST("test", true); private final String name; private final boolean proxy; diff --git a/common/src/com/elikill58/negativity/universal/ban/BanResult.java b/common/src/com/elikill58/negativity/universal/ban/BanResult.java index 41c4b45c2..87481f29f 100644 --- a/common/src/com/elikill58/negativity/universal/ban/BanResult.java +++ b/common/src/com/elikill58/negativity/universal/ban/BanResult.java @@ -1,5 +1,7 @@ package com.elikill58.negativity.universal.ban; +import java.util.Objects; + public class BanResult { private final BanResultType resultType; @@ -32,6 +34,24 @@ public boolean isSuccess() { return getResultType().isSuccess(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BanResult banResult = (BanResult) o; + return resultType == banResult.resultType && Objects.equals(ban, banResult.ban); + } + + @Override + public int hashCode() { + return Objects.hash(resultType, ban); + } + + @Override + public String toString() { + return "BanResult{resultType=" + resultType + ", ban=" + ban + '}'; + } + public enum BanResultType { ALREADY_BANNED(false, "Already banned"), diff --git a/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseActiveBanStorage.java b/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseActiveBanStorage.java index 1c4ff94f7..0ad7987b3 100644 --- a/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseActiveBanStorage.java +++ b/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseActiveBanStorage.java @@ -25,7 +25,7 @@ public DatabaseActiveBanStorage() { try { Connection connection = Database.getConnection(); if (connection != null) { - DatabaseMigrator.executeRemainingMigrations(connection, "bans/active"); + DatabaseMigrator.executeRemainingMigrations(connection, Database.getType().getType(), "bans/active"); } } catch (Exception e) { Adapter.getAdapter().getLogger().error("Failed to execute active bans database migration: " + e.getMessage()); diff --git a/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseBanLogsStorage.java b/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseBanLogsStorage.java index 956bb836b..31ea324c8 100644 --- a/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseBanLogsStorage.java +++ b/common/src/com/elikill58/negativity/universal/ban/storage/DatabaseBanLogsStorage.java @@ -22,7 +22,7 @@ public DatabaseBanLogsStorage() { try { Connection connection = Database.getConnection(); if (connection != null) { - DatabaseMigrator.executeRemainingMigrations(connection, "bans/logs"); + DatabaseMigrator.executeRemainingMigrations(connection, Database.getType().getType(), "bans/logs"); } } catch (Exception e) { Adapter.getAdapter().getLogger().error("Failed to execute ban logs database migration: " + e.getMessage()); diff --git a/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseMigrator.java b/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseMigrator.java index d5141969a..e229cce27 100644 --- a/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseMigrator.java +++ b/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseMigrator.java @@ -14,9 +14,12 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; @@ -27,32 +30,36 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class DatabaseMigrator { - - // Matches an int from the start of the string - private static final Pattern FILE_VERSION_PATTERN = Pattern.compile("^(\\d*)"); + + // Matches the full migration file name, example: 000-postgresql-Create-Initial-Table.sql + // 000 -> int; the version + // postgresql -> optional string; group named 'dbvariant' + // Create-Initial-Table -> string; migration name + // .sql -> literal; file extension + private static final Pattern FILE_NAME_PATTERN = Pattern.compile("^(\\d*)(-(?\\p{Lower}*))?-(.*)\\.sql$"); // Flexible pattern for the in-house statement separator comment "-- ;" // The MULTILINE flag is used to easily match an entire line, especially to include the optional comment text after the semicolon private static final Pattern STATEMENT_SEPARATOR_PATTERN = Pattern.compile("-- \\s*;.*?$", Pattern.MULTILINE); // Matches a blank string private static final Pattern BLANK_PATTERN = Pattern.compile("\\s*"); - - public static MigrationResult executeRemainingMigrations(Connection connection, String subsystem) throws SQLException { + + public static MigrationResult executeRemainingMigrations(Connection connection, String databaseType, String subsystem) throws SQLException { try { return CompletableFuture.supplyAsync(() -> { try (PreparedStatement createMigrationsStm = connection.prepareStatement( - "CREATE TABLE IF NOT EXISTS negativity_migrations_history (subsystem VARCHAR(32), version INT, update_time TIMESTAMP DEFAULT NOW())"); + "CREATE TABLE IF NOT EXISTS negativity_migrations_history (subsystem VARCHAR(32), version INT, update_time TIMESTAMP DEFAULT NOW())"); // Gets the latest version of this database PreparedStatement getCurrentVersion = connection.prepareStatement("SELECT version FROM negativity_migrations_history WHERE subsystem = ? ORDER BY version DESC LIMIT 1")) { createMigrationsStm.executeUpdate(); - + getCurrentVersion.setString(1, subsystem); ResultSet result = getCurrentVersion.executeQuery(); int previousVersion = -1; if (result.next()) { previousVersion = result.getInt(1); } - - int newVersion = doExecuteRemainingMigrations(connection, previousVersion, subsystem); + + int newVersion = doExecuteRemainingMigrations(connection, databaseType, previousVersion, subsystem); if (newVersion >= 0) { // At least one migration was executed try (PreparedStatement insertRecordStm = connection.prepareStatement("INSERT INTO negativity_migrations_history (version, subsystem) VALUES (?, ?)")) { @@ -72,9 +79,9 @@ public static MigrationResult executeRemainingMigrations(Connection connection, return null; } } - - private static int doExecuteRemainingMigrations(Connection connection, int currentVersion, String subsystem) throws SQLException { - RemainingMigrations migrationsToDo = getMigrationsToExecute(currentVersion, subsystem); + + private static int doExecuteRemainingMigrations(Connection connection, String databaseType, int currentVersion, String subsystem) throws SQLException { + RemainingMigrations migrationsToDo = getMigrationsToExecute(databaseType, currentVersion, subsystem); if (migrationsToDo == null) { return -1; } @@ -91,93 +98,135 @@ private static int doExecuteRemainingMigrations(Connection connection, int curre } return migrationsToDo.highestMigrationVersion; } - + @Nullable - private static RemainingMigrations getMigrationsToExecute(int currentVersion, String subsystem) { + private static RemainingMigrations getMigrationsToExecute(String databaseType, int currentVersion, String subsystem) { try { URI migrationsDirUri = DatabaseMigrator.class.getResource("/databaseMigrations").toURI(); if (migrationsDirUri.getScheme().equals("jar")) { try (FileSystem jarFs = FileSystems.newFileSystem(migrationsDirUri, Collections.emptyMap())) { - return getMigrationsToExecute(jarFs.getPath("/databaseMigrations", subsystem), currentVersion); + return getMigrationsToExecute(databaseType, jarFs.getPath("/databaseMigrations", subsystem), currentVersion); } } - return getMigrationsToExecute(Paths.get(migrationsDirUri).resolve(subsystem), currentVersion); + return getMigrationsToExecute(databaseType, Paths.get(migrationsDirUri).resolve(subsystem), currentVersion); } catch (URISyntaxException | IOException e) { e.printStackTrace(); } return null; } - - private static RemainingMigrations getMigrationsToExecute(Path migrationsDir, int currentVersion) throws IOException { + + private static RemainingMigrations getMigrationsToExecute(String databaseType, Path migrationsDir, int currentVersion) throws IOException { int highestMigrationVersion = -1; + Set scriptsWithVariant = new HashSet<>(); List migrationScripts = new ArrayList<>(); try (Stream migrationFiles = Files.list(migrationsDir)) { for (Path path : migrationFiles.collect(Collectors.toList())) { if (!Files.isRegularFile(path)) { continue; } + String fileName = path.getFileName().toString(); - Matcher fileVersionMatcher = FILE_VERSION_PATTERN.matcher(fileName); - if (!fileVersionMatcher.find()) { + Matcher fileNameMatcher = FILE_NAME_PATTERN.matcher(fileName); + if (!fileNameMatcher.matches()) { continue; } + + @Nullable String databaseVariant = fileNameMatcher.group("dbvariant"); + if (databaseVariant != null && !databaseType.equals(databaseVariant)) { + continue; + } + + String migrationName = fileNameMatcher.group(fileNameMatcher.groupCount()); + + String rawMigrationVersion = fileNameMatcher.group(1); try { - int migrationVersion = Integer.parseInt(fileVersionMatcher.group()); + int migrationVersion = Integer.parseInt(rawMigrationVersion); if (migrationVersion > currentVersion) { String rawScript = new String(Files.readAllBytes(path)); - migrationScripts.add(new MigrationScript(STATEMENT_SEPARATOR_PATTERN.split(rawScript), migrationVersion)); + String[] statements = STATEMENT_SEPARATOR_PATTERN.split(rawScript); + migrationScripts.add(new MigrationScript(statements, migrationVersion, databaseVariant, migrationName)); + + if (databaseVariant != null) { + scriptsWithVariant.add(migrationVersion); + } + if (migrationVersion > highestMigrationVersion) { highestMigrationVersion = migrationVersion; } } } catch (NumberFormatException ignore) { + throw new IllegalStateException("Migration file name does not have a valid version number: '" + rawMigrationVersion + "'"); } } } + + migrationScripts.removeIf(script -> scriptsWithVariant.contains(script.version) && script.variant == null); migrationScripts.sort(Comparator.comparingInt(script -> script.version)); + + Set versions = new HashSet<>(); + for (MigrationScript script : migrationScripts) { + if (!versions.add(script.version)) { + throw new IllegalStateException("Found scripts with duplicate version " + script); + } + } + return new RemainingMigrations(migrationScripts, highestMigrationVersion); } - + private static class RemainingMigrations { - + public final List migrationScripts; public final int highestMigrationVersion; - + private RemainingMigrations(List migrationScripts, int highestMigrationVersion) { this.migrationScripts = migrationScripts; this.highestMigrationVersion = highestMigrationVersion; } } - + private static class MigrationScript { - + public final String[] statements; public final int version; - - private MigrationScript(String[] statements, int version) { + public final @Nullable String variant; + public final String name; + + private MigrationScript(String[] statements, int version, @Nullable String variant, String name) { this.statements = statements; this.version = version; + this.variant = variant; + this.name = name; + } + + @Override + public String toString() { + return "MigrationScript{" + + "statements=" + Arrays.toString(statements) + + ", version=" + version + + ", variant='" + variant + '\'' + + ", name='" + name + '\'' + + '}'; } } - + public static class MigrationResult { - + private final int previousVersion; private final int newVersion; - + public MigrationResult(int previousVersion, int newVersion) { this.previousVersion = previousVersion; this.newVersion = newVersion; } - + public int getPreviousVersion() { return previousVersion; } - + public int getNewVersion() { return newVersion; } - + public boolean hasUpdated() { return newVersion > previousVersion; } diff --git a/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseNegativityAccountStorage.java b/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseNegativityAccountStorage.java index c28828a9e..513668bdd 100644 --- a/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseNegativityAccountStorage.java +++ b/common/src/com/elikill58/negativity/universal/dataStorage/database/DatabaseNegativityAccountStorage.java @@ -28,7 +28,7 @@ public DatabaseNegativityAccountStorage() { try { Connection connection = Database.getConnection(); if (connection != null) { - DatabaseMigrator.executeRemainingMigrations(connection, "accounts"); + DatabaseMigrator.executeRemainingMigrations(connection, Database.getType().getType(), "accounts"); } } catch (SQLException e) { e.printStackTrace(); diff --git a/common/src/com/elikill58/negativity/universal/utils/FileUtils.java b/common/src/com/elikill58/negativity/universal/utils/FileUtils.java index 0d46119a2..90b4654ce 100644 --- a/common/src/com/elikill58/negativity/universal/utils/FileUtils.java +++ b/common/src/com/elikill58/negativity/universal/utils/FileUtils.java @@ -12,6 +12,10 @@ public class FileUtils { public static Path cleanDirectory(Path directory) throws IOException { + if (!Files.exists(directory)) { + return directory; + } + if (!Files.isDirectory(directory)) { throw new NotDirectoryException(directory.toAbsolutePath().toString()); } diff --git a/common/src/com/elikill58/negativity/universal/utils/UniversalUtils.java b/common/src/com/elikill58/negativity/universal/utils/UniversalUtils.java index 223352465..785fb1811 100644 --- a/common/src/com/elikill58/negativity/universal/utils/UniversalUtils.java +++ b/common/src/com/elikill58/negativity/universal/utils/UniversalUtils.java @@ -287,7 +287,7 @@ public static boolean isValidName(String name) { /** * Check if the given string contains a chinese characters - * + * * @param s The string where we are looking for chinese char * @return true if there is a chinese char */ @@ -337,20 +337,29 @@ public static Path copyBundledFile(String name, Path destFile) throws IOExceptio } public static Configuration loadConfig(File configFile, String configName) { - if(!configFile.exists()) { + if (!configFile.exists()) { configFile.getParentFile().mkdirs(); try { URI migrationsDirUri = UniversalUtils.class.getResource("/assets/negativity").toURI(); if (migrationsDirUri.getScheme().equals("jar")) { try (FileSystem jarFs = FileSystems.newFileSystem(migrationsDirUri, Collections.emptyMap())) { Path cheatPath = jarFs.getPath("/assets/negativity", configName); - if(Files.isRegularFile(cheatPath)) { + if (Files.isRegularFile(cheatPath)) { Files.copy(cheatPath, Paths.get(configFile.toURI())); } else { Adapter.getAdapter().getLogger().error("Cannot load config."); return null; } } + } else { + Path cheatPath = Paths.get(migrationsDirUri).resolve(configName); + if (Files.isRegularFile(cheatPath)) { + Path target = Paths.get(configFile.toURI()); + Files.copy(cheatPath, target); + } else { + Adapter.getAdapter().getLogger().error("Cannot load config."); + return null; + } } } catch (URISyntaxException | IOException e) { e.printStackTrace(); diff --git a/common/src/com/elikill58/negativity/universal/verif/storage/database/DatabaseVerificationStorage.java b/common/src/com/elikill58/negativity/universal/verif/storage/database/DatabaseVerificationStorage.java index 332918b03..70650bd8a 100644 --- a/common/src/com/elikill58/negativity/universal/verif/storage/database/DatabaseVerificationStorage.java +++ b/common/src/com/elikill58/negativity/universal/verif/storage/database/DatabaseVerificationStorage.java @@ -28,7 +28,7 @@ public DatabaseVerificationStorage() { try { Connection connection = Database.getConnection(); if (connection != null) { - DatabaseMigrator.executeRemainingMigrations(connection, "verifications"); + DatabaseMigrator.executeRemainingMigrations(connection, Database.getType().getType(), "verifications"); } } catch (SQLException e) { e.printStackTrace(); diff --git a/common/testSrc/com/elikill58/negativity/testFramework/BanProcessorTestInvocationContextProvider.java b/common/testSrc/com/elikill58/negativity/testFramework/BanProcessorTestInvocationContextProvider.java new file mode 100644 index 000000000..46905ce57 --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/BanProcessorTestInvocationContextProvider.java @@ -0,0 +1,117 @@ +package com.elikill58.negativity.testFramework; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestTemplateInvocationContext; +import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; + +import com.elikill58.negativity.universal.ban.processor.BanProcessor; +import com.elikill58.negativity.universal.ban.processor.NegativityBanProcessor; +import com.elikill58.negativity.universal.ban.storage.DatabaseActiveBanStorage; +import com.elikill58.negativity.universal.ban.storage.DatabaseBanLogsStorage; +import com.elikill58.negativity.universal.ban.storage.FileActiveBanStorage; +import com.elikill58.negativity.universal.ban.storage.FileBanLogsStorage; + +public class BanProcessorTestInvocationContextProvider implements TestTemplateInvocationContextProvider { + + @Override + public boolean supportsTestTemplate(ExtensionContext context) { + return true; + } + + @Override + public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { + List invocationContexts = new ArrayList<>(); + invocationContexts.add(new FileBanProcessorTestInvocationContext(context, Paths.get("bans"))); + + String availableDatabases = context.getConfigurationParameter("negativity.databases").orElse(null); + if (availableDatabases != null) { + String[] split = availableDatabases.split(","); + for (String database : split) { + invocationContexts.add(new DatabaseBanProcessorTestInvocationContext(context, database)); + } + } + + return invocationContexts.stream(); + } + + private static class DatabaseBanProcessorTestInvocationContext implements TestTemplateInvocationContext { + + private final ExtensionContext context; + private final String databaseName; + + public DatabaseBanProcessorTestInvocationContext(ExtensionContext context, String databaseName) { + this.context = context; + this.databaseName = databaseName; + } + + @Override + public String getDisplayName(int invocationIndex) { + return context.getDisplayName() + " [database: " + databaseName + "]"; + } + + @Override + public List getAdditionalExtensions() { + List extensions = new ArrayList<>(); + extensions.add(new SetupNegativityTestExtension()); + extensions.add(new SetupDatabaseTestExtension(databaseName)); + extensions.add(new ParameterResolver() { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.getParameter().getType() == BanProcessor.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return new NegativityBanProcessor(new DatabaseActiveBanStorage(), new DatabaseBanLogsStorage()); + } + }); + return extensions; + } + + } + + private static class FileBanProcessorTestInvocationContext implements TestTemplateInvocationContext { + + private final ExtensionContext context; + private final Path baseDir; + + public FileBanProcessorTestInvocationContext(ExtensionContext context, Path baseDir) { + this.context = context; + this.baseDir = baseDir; + } + + @Override + public String getDisplayName(int invocationIndex) { + return context.getDisplayName() + " [file]"; + } + + @Override + public List getAdditionalExtensions() { + List extensions = new ArrayList<>(); + extensions.add(new CleanDirectoryTestExtension(baseDir)); + extensions.add(new SetupNegativityTestExtension()); + extensions.add(new ParameterResolver() { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterContext.getParameter().getType() == BanProcessor.class; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { + return new NegativityBanProcessor(new FileActiveBanStorage(baseDir), new FileBanLogsStorage(baseDir)); + } + }); + return extensions; + } + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/CleanDirectoryTestExtension.java b/common/testSrc/com/elikill58/negativity/testFramework/CleanDirectoryTestExtension.java new file mode 100644 index 000000000..cd4e1e053 --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/CleanDirectoryTestExtension.java @@ -0,0 +1,29 @@ +package com.elikill58.negativity.testFramework; + +import java.nio.file.Path; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.elikill58.negativity.universal.utils.FileUtils; + +public class CleanDirectoryTestExtension implements BeforeEachCallback, AfterEachCallback { + + private final Path directory; + + public CleanDirectoryTestExtension(Path directory) { + this.directory = directory; + } + + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + FileUtils.cleanDirectory(directory); + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + FileUtils.cleanDirectory(directory); + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProvider.java b/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProvider.java new file mode 100644 index 000000000..3bfbafae0 --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProvider.java @@ -0,0 +1,36 @@ +package com.elikill58.negativity.testFramework; + +import java.util.Collections; +import java.util.List; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import com.elikill58.negativity.universal.translation.TranslationProvider; + +public class DummyTranslationProvider implements TranslationProvider { + + @Override + public @Nullable String get(String key) { + return key; + } + + @Override + public @Nullable List getList(String key) { + return Collections.singletonList(key); + } + + @Override + public String applyPlaceholders(String raw, Object... placeholders) { + StringBuilder placeholdersBuilder = new StringBuilder(); + for (int i = 0; i < placeholders.length + 1; i += 2) { + if (i == 0) { + placeholdersBuilder.append(", "); + } + + placeholdersBuilder.append(placeholders[i]); + placeholdersBuilder.append('='); + placeholdersBuilder.append(placeholders[i + 1]); + } + return raw + " :: " + placeholdersBuilder; + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProviderFactory.java b/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProviderFactory.java new file mode 100644 index 000000000..18a4396ba --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/DummyTranslationProviderFactory.java @@ -0,0 +1,19 @@ +package com.elikill58.negativity.testFramework; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import com.elikill58.negativity.universal.translation.TranslationProvider; +import com.elikill58.negativity.universal.translation.TranslationProviderFactory; + +public class DummyTranslationProviderFactory implements TranslationProviderFactory { + + @Override + public @Nullable TranslationProvider createTranslationProvider(String language) { + return createFallbackTranslationProvider(); + } + + @Override + public @Nullable TranslationProvider createFallbackTranslationProvider() { + return new DummyTranslationProvider(); + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/SetupDatabaseTestExtension.java b/common/testSrc/com/elikill58/negativity/testFramework/SetupDatabaseTestExtension.java new file mode 100644 index 000000000..b9271177f --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/SetupDatabaseTestExtension.java @@ -0,0 +1,85 @@ +package com.elikill58.negativity.testFramework; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.elikill58.negativity.api.yaml.config.Configuration; +import com.elikill58.negativity.universal.Adapter; +import com.elikill58.negativity.universal.Database; + +public class SetupDatabaseTestExtension implements BeforeEachCallback, AfterEachCallback { + + private final String databaseName; + + public SetupDatabaseTestExtension(String databaseName) { + this.databaseName = databaseName; + } + + private @NonNull String getParameter(ExtensionContext context, String key) { + Optional url = context.getConfigurationParameter("negativity.db." + databaseName + "." + key); + Assumptions.assumeTrue(url.isPresent(), "Missing " + key + " of database '" + databaseName + "'"); + return url.get(); + } + + @Override + public void beforeEach(ExtensionContext context) throws Exception { + Adapter adapter = Adapter.getAdapter(); + Configuration config = adapter.getConfig(); + config.set("Database.isActive", true); + config.set("Database.url", getParameter(context, "url")); + config.set("Database.user", getParameter(context, "user")); + config.set("Database.password", getParameter(context, "password")); + config.set("Database.type", getParameter(context, "type")); + adapter.reload(); + Assumptions.assumeTrue(Database.hasCustom, "Could not connect to database, skipping tests."); + Connection connection = Database.getConnection(); + if (connection != null) { + // Clearing the tables before each test guarantees proper test isolation on the database end + clearDatabaseTables(connection); + } + } + + @Override + public void afterEach(ExtensionContext context) throws Exception { + Connection connection = Database.getConnection(); + if (connection != null) { + // Clearing the tables after each test guarantees proper test isolation on the database end + clearDatabaseTables(connection); + } + Database.close(); + } + + private static void clearDatabaseTables(Connection connection) throws SQLException { + try (PreparedStatement listTablesStm = connection.prepareStatement("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'negativity_tests'")) { + ResultSet listTablesResult = listTablesStm.executeQuery(); + connection.setAutoCommit(false); + while (listTablesResult.next()) { + String tableName = listTablesResult.getString("TABLE_NAME"); + if (!tableName.startsWith("negativity_")) { + continue; + } + + try (PreparedStatement dropTableStm = connection.prepareStatement("DROP TABLE " + tableName)) { + dropTableStm.executeUpdate(); + } + } + connection.commit(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + if (!connection.getAutoCommit()) { + connection.rollback(); + } + } finally { + connection.setAutoCommit(true); + } + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/SetupNegativityTestExtension.java b/common/testSrc/com/elikill58/negativity/testFramework/SetupNegativityTestExtension.java new file mode 100644 index 000000000..026c22fd8 --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/SetupNegativityTestExtension.java @@ -0,0 +1,20 @@ +package com.elikill58.negativity.testFramework; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +import com.elikill58.negativity.universal.Adapter; + +public class SetupNegativityTestExtension implements BeforeEachCallback, AfterEachCallback { + + @Override + public void beforeEach(ExtensionContext context) { + Adapter.setAdapter(new TestAdapter()); + } + + @Override + public void afterEach(ExtensionContext context) { + Adapter.getAdapter().reloadConfig(); + } +} diff --git a/common/testSrc/com/elikill58/negativity/testFramework/TestAdapter.java b/common/testSrc/com/elikill58/negativity/testFramework/TestAdapter.java new file mode 100644 index 000000000..a1ebdddbb --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/testFramework/TestAdapter.java @@ -0,0 +1,216 @@ +package com.elikill58.negativity.testFramework; + +import java.io.File; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Logger; + +import com.elikill58.negativity.api.entity.FakePlayer; +import com.elikill58.negativity.api.entity.OfflinePlayer; +import com.elikill58.negativity.api.entity.Player; +import com.elikill58.negativity.api.inventory.Inventory; +import com.elikill58.negativity.api.inventory.NegativityHolder; +import com.elikill58.negativity.api.item.ItemBuilder; +import com.elikill58.negativity.api.item.ItemRegistrar; +import com.elikill58.negativity.api.item.Material; +import com.elikill58.negativity.api.location.Location; +import com.elikill58.negativity.api.location.World; +import com.elikill58.negativity.api.plugin.ExternalPlugin; +import com.elikill58.negativity.api.yaml.config.Configuration; +import com.elikill58.negativity.universal.Adapter; +import com.elikill58.negativity.universal.Negativity; +import com.elikill58.negativity.universal.Platform; +import com.elikill58.negativity.universal.Scheduler; +import com.elikill58.negativity.universal.Version; +import com.elikill58.negativity.universal.account.NegativityAccountManager; +import com.elikill58.negativity.universal.logger.JavaLoggerAdapter; +import com.elikill58.negativity.universal.logger.LoggerAdapter; +import com.elikill58.negativity.universal.translation.TranslationProviderFactory; + +public class TestAdapter extends Adapter { + + private final LoggerAdapter logger = new JavaLoggerAdapter(Logger.getLogger("NegativityTest")); + private Configuration configuration = new Configuration(); + private final File dataFolder = new File("./"); + + @Override + public Platform getPlatformID() { + return Platform.TEST; + } + + @Override + public Configuration getConfig() { + return this.configuration; + } + + @Override + public File getDataFolder() { + return this.dataFolder; + } + + @Override + public LoggerAdapter getLogger() { + return this.logger; + } + + @Override + public void debug(String msg) { + this.logger.info(msg); + } + + @Override + public TranslationProviderFactory getPlatformTranslationProviderFactory() { + return new DummyTranslationProviderFactory(); + } + + @Override + public void reload() { + Negativity.loadNegativity(); + } + + @Override + public String getVersion() { + return "test-dev"; + } + + @Override + public Version getServerVersion() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public String getPluginVersion() { + return "test-dev"; + } + + @Override + public void reloadConfig() { + this.configuration = new Configuration(); + } + + @Override + public NegativityAccountManager getAccountManager() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public void runConsoleCommand(String cmd) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public CompletableFuture isUsingMcLeaks(UUID playerId) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public List getOnlinePlayersUUID() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public List getOnlinePlayers() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public double[] getTPS() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public double getLastTPS() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ItemRegistrar getItemRegistrar() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ItemBuilder createItemBuilder(Material type) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ItemBuilder createItemBuilder(String type) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ItemBuilder createSkullItemBuilder(Player owner) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ItemBuilder createSkullItemBuilder(OfflinePlayer owner) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public Location createLocation(World w, double x, double y, double z) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public Inventory createInventory(String inventoryName, int size, NegativityHolder holder) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public OfflinePlayer getOfflinePlayer(String name) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public OfflinePlayer getOfflinePlayer(UUID uuid) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public Player getPlayer(String name) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public Player getPlayer(UUID uuid) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public FakePlayer createFakePlayer(Location loc, String name) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public void sendMessageRunnableHover(Player p, String message, String hover, String command) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public boolean hasPlugin(String name) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public ExternalPlugin getPlugin(String name) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public List getDependentPlugins() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public void runSync(Runnable call) { + throw new UnsupportedOperationException("To be implemented"); // TODO + } + + @Override + public Scheduler getScheduler() { + throw new UnsupportedOperationException("To be implemented"); // TODO + } +} diff --git a/common/testSrc/com/elikill58/negativity/universal/ban/BanProcessorTests.java b/common/testSrc/com/elikill58/negativity/universal/ban/BanProcessorTests.java new file mode 100644 index 000000000..73648c843 --- /dev/null +++ b/common/testSrc/com/elikill58/negativity/universal/ban/BanProcessorTests.java @@ -0,0 +1,26 @@ +package com.elikill58.negativity.universal.ban; + +import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.elikill58.negativity.testFramework.BanProcessorTestInvocationContextProvider; +import com.elikill58.negativity.universal.ban.processor.BanProcessor; + +@ExtendWith(BanProcessorTestInvocationContextProvider.class) +public class BanProcessorTests { + + @TestTemplate + public void sameBanTwice(BanProcessor processor) { + UUID playerId = UUID.randomUUID(); + long executionTime = System.currentTimeMillis(); + long expirationTime = executionTime + 10_000; + Ban originalBan = new Ban(playerId, "Ban reason", "Ban source", BanType.MOD, expirationTime, "Cheat 1, Cheat 2", null, BanStatus.ACTIVE, executionTime); + BanResult result = processor.executeBan(originalBan); + Assertions.assertEquals(new BanResult(BanResult.BanResultType.DONE, originalBan), result); + BanResult result2 = processor.executeBan(originalBan); + Assertions.assertEquals(new BanResult(BanResult.BanResultType.ALREADY_BANNED, null), result2); + } +} diff --git a/settings.gradle b/settings.gradle index 28a58ce5a..5ac4ee6b6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,4 +5,3 @@ include 'common', 'spigot', 'bungee', 'sponge', 'velocity'//, 'sponge8' include 'common:integrations:litebans', 'common:integrations:advancedban', 'common:integrations:dkbans', 'common:integrations:viaversion', 'spigot:integrations:essentials', 'spigot:integrations:floodgate', 'spigot:integrations:gadgetsmenu', 'spigot:integrations:maxbans', 'spigot:integrations:protocolsupport', 'spigot:integrations:worldguard' -