diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java index f11e6d2a..9f24592f 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/AccountsCommand.java @@ -27,6 +27,7 @@ public class AccountsCommand implements ExecutableCommand { @Override public void executeCommand(final CommandSender sender, List arguments) { + // TODO #1366: last IP vs. registration IP? final String playerName = arguments.isEmpty() ? sender.getName() : arguments.get(0); // Assumption: a player name cannot contain '.' @@ -52,6 +53,9 @@ public class AccountsCommand implements ExecutableCommand { if (auth == null) { commonService.send(sender, MessageKey.UNKNOWN_USER); return; + } else if (auth.getLastIp() == null) { + sender.sendMessage("No known last IP address for player"); + return; } List accountList = dataSource.getAllAuthsByIp(auth.getLastIp()); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java index b2ae4006..6e126b90 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/CountryLookup.java @@ -72,10 +72,13 @@ class CountryLookup implements DebugSection { sender.sendMessage("Note: if " + ProtectionSettings.ENABLE_PROTECTION + " is false no country is blocked"); } + // TODO #1366: Extend with registration IP? private void outputInfoForPlayer(CommandSender sender, String name) { PlayerAuth auth = dataSource.getAuth(name); if (auth == null) { sender.sendMessage("No player with name '" + name + "'"); + } else if (auth.getLastIp() == null) { + sender.sendMessage("No last IP address known for '" + name + "'"); } else { sender.sendMessage("Player '" + name + "' has IP address " + auth.getLastIp()); outputInfoForIpAddr(sender, auth.getLastIp()); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java index d6c4e2f4..32750546 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugCommand.java @@ -21,7 +21,7 @@ public class DebugCommand implements ExecutableCommand { private static final Set> SECTION_CLASSES = ImmutableSet.of( PermissionGroups.class, DataStatistics.class, CountryLookup.class, PlayerAuthViewer.class, InputValidator.class, LimboPlayerViewer.class, CountryLookup.class, HasPermissionChecker.class, TestEmailSender.class, - SpawnLocationViewer.class, MySqlDefaultChanger.class); + SpawnLocationViewer.class, MySqlDefaultChanger.class, SqliteMigrater.class); @Inject private Factory debugSectionFactory; diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java index d98ce746..14b07ac9 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtils.java @@ -2,6 +2,8 @@ package fr.xephi.authme.command.executable.authme.debug; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; import org.bukkit.Location; import java.lang.reflect.Field; @@ -85,7 +87,7 @@ final class DebugSectionUtils { * @param function the function to apply to the map * @param the result type of the function * - * @return player names for which there is a LimboPlayer (or error message upon failure) + * @return the value of the function applied to the map, or null upon error */ static U applyToLimboPlayersMap(LimboService limboService, Function function) { Field limboPlayerEntriesField = getLimboPlayerEntriesField(); @@ -98,4 +100,29 @@ final class DebugSectionUtils { } return null; } + + static T castToTypeOrNull(Object object, Class clazz) { + return clazz.isInstance(object) ? clazz.cast(object) : null; + } + + /** + * Unwraps the "cache data source" and returns the underlying source. Returns the + * same as the input argument otherwise. + * + * @param dataSource the data source to unwrap if applicable + * @return the non-cache data source + */ + static DataSource unwrapSourceFromCacheDataSource(DataSource dataSource) { + if (dataSource instanceof CacheDataSource) { + try { + Field source = CacheDataSource.class.getDeclaredField("source"); + source.setAccessible(true); + return (DataSource) source.get(dataSource); + } catch (NoSuchFieldException | IllegalAccessException e) { + ConsoleLogger.logException("Could not get source of CacheDataSource:", e); + return null; + } + } + return dataSource; + } } diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java index 7b2f7636..98612b15 100644 --- a/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChanger.java @@ -3,7 +3,6 @@ package fr.xephi.authme.command.executable.authme.debug; import ch.jalu.configme.properties.Property; import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; -import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.MySQL; import fr.xephi.authme.permission.DebugSectionPermissions; @@ -15,21 +14,23 @@ import org.bukkit.command.CommandSender; import javax.annotation.PostConstruct; import javax.inject.Inject; -import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.castToTypeOrNull; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.unwrapSourceFromCacheDataSource; import static fr.xephi.authme.data.auth.PlayerAuth.DB_EMAIL_DEFAULT; +import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_IP_DEFAULT; import static fr.xephi.authme.data.auth.PlayerAuth.DB_LAST_LOGIN_DEFAULT; +import static fr.xephi.authme.datasource.SqlDataSourceUtils.isNotNullColumn; import static java.lang.String.format; /** @@ -48,10 +49,7 @@ class MySqlDefaultChanger implements DebugSection { @PostConstruct void setMySqlField() { - DataSource dataSource = unwrapSourceFromCacheDataSource(this.dataSource); - if (dataSource instanceof MySQL) { - this.mySql = (MySQL) dataSource; - } + this.mySql = castToTypeOrNull(unwrapSourceFromCacheDataSource(this.dataSource), MySQL.class); } @Override @@ -213,24 +211,6 @@ class MySqlDefaultChanger implements DebugSection { } } - private boolean isNotNullColumn(DatabaseMetaData metaData, String tableName, - String columnName) throws SQLException { - try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { - if (!rs.next()) { - throw new IllegalStateException("Did not find meta data for column '" + columnName - + "' while migrating not-null columns (this should never happen!)"); - } - - int nullableCode = rs.getInt("NULLABLE"); - if (nullableCode == DatabaseMetaData.columnNoNulls) { - return true; - } else if (nullableCode == DatabaseMetaData.columnNullableUnknown) { - ConsoleLogger.warning("Unknown nullable status for column '" + columnName + "'"); - } - } - return false; - } - /** * Gets the Connection object from the MySQL data source. * @@ -248,28 +228,6 @@ class MySqlDefaultChanger implements DebugSection { } } - /** - * Unwraps the "cache data source" and returns the underlying source. Returns the - * same as the input argument otherwise. - * - * @param dataSource the data source to unwrap if applicable - * @return the non-cache data source - */ - @VisibleForTesting - static DataSource unwrapSourceFromCacheDataSource(DataSource dataSource) { - if (dataSource instanceof CacheDataSource) { - try { - Field source = CacheDataSource.class.getDeclaredField("source"); - source.setAccessible(true); - return (DataSource) source.get(dataSource); - } catch (NoSuchFieldException | IllegalAccessException e) { - ConsoleLogger.logException("Could not get source of CacheDataSource:", e); - return null; - } - } - return dataSource; - } - private static > E matchToEnum(List arguments, int index, Class clazz) { if (arguments.size() <= index) { return null; @@ -290,6 +248,11 @@ class MySqlDefaultChanger implements DebugSection { LASTLOGIN(DatabaseSettings.MYSQL_COL_LASTLOGIN, "BIGINT", "BIGINT NOT NULL DEFAULT 0", DB_LAST_LOGIN_DEFAULT), + LASTIP(DatabaseSettings.MYSQL_COL_LAST_IP, + "VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin", + "VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '127.0.0.1'", + DB_LAST_IP_DEFAULT), + EMAIL(DatabaseSettings.MYSQL_COL_EMAIL, "VARCHAR(255)", "VARCHAR(255) NOT NULL DEFAULT 'your@email.com'", DB_EMAIL_DEFAULT); diff --git a/src/main/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigrater.java b/src/main/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigrater.java new file mode 100644 index 00000000..359775e0 --- /dev/null +++ b/src/main/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigrater.java @@ -0,0 +1,178 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import fr.xephi.authme.ConsoleLogger; +import fr.xephi.authme.datasource.Columns; +import fr.xephi.authme.datasource.DataSource; +import fr.xephi.authme.datasource.SQLite; +import fr.xephi.authme.permission.DebugSectionPermissions; +import fr.xephi.authme.permission.PermissionNode; +import fr.xephi.authme.settings.Settings; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import fr.xephi.authme.util.RandomStringUtils; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.lang.reflect.Field; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.castToTypeOrNull; +import static fr.xephi.authme.command.executable.authme.debug.DebugSectionUtils.unwrapSourceFromCacheDataSource; +import static org.bukkit.ChatColor.BOLD; +import static org.bukkit.ChatColor.GOLD; + +/** + * Performs a migration on the SQLite data source if necessary. + */ +class SqliteMigrater implements DebugSection { + + @Inject + private DataSource dataSource; + + @Inject + private Settings settings; + + private SQLite sqLite; + + private String confirmationCode; + + @PostConstruct + void setSqLiteField() { + this.sqLite = castToTypeOrNull(unwrapSourceFromCacheDataSource(this.dataSource), SQLite.class); + } + + @Override + public String getName() { + return "migratesqlite"; + } + + @Override + public String getDescription() { + return "Migrates the SQLite database"; + } + + // A migration can be forced even if SQLite says it doesn't need a migration by adding "force" as second argument + @Override + public void execute(CommandSender sender, List arguments) { + if (sqLite == null) { + sender.sendMessage("This command migrates SQLite. You are currently not using a SQLite database."); + return; + } + + if (!isMigrationRequired() && !isMigrationForced(arguments)) { + sender.sendMessage("Good news! No migration is required of your database"); + } else if (checkConfirmationCodeAndInformSenderOnMismatch(sender, arguments)) { + final String tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + final Columns columns = new Columns(settings); + try { + recreateDatabaseWithNewDefinitions(tableName, columns); + sender.sendMessage(ChatColor.GREEN + "Successfully migrated your SQLite database!"); + } catch (SQLException e) { + ConsoleLogger.logException("Failed to migrate SQLite database", e); + sender.sendMessage(ChatColor.RED + + "An error occurred during SQLite migration. Please check the logs!"); + } + } + } + + private boolean checkConfirmationCodeAndInformSenderOnMismatch(CommandSender sender, List arguments) { + boolean isMatch = !arguments.isEmpty() && arguments.get(0).equalsIgnoreCase(confirmationCode); + if (isMatch) { + confirmationCode = null; + return true; + } else { + confirmationCode = RandomStringUtils.generate(4).toUpperCase(); + sender.sendMessage(new String[]{ + BOLD.toString() + GOLD + "Please create a backup of your SQLite database before running this command!", + "Either copy your DB file or run /authme backup. Afterwards,", + String.format("run '/authme debug %s %s' to perform the migration. " + + "The code confirms that you've made a backup!", getName(), confirmationCode) + }); + return false; + } + } + + @Override + public PermissionNode getRequiredPermission() { + return DebugSectionPermissions.MIGRATE_SQLITE; + } + + private boolean isMigrationRequired() { + Connection connection = getConnection(sqLite); + try { + DatabaseMetaData metaData = connection.getMetaData(); + return sqLite.isMigrationRequired(metaData); + } catch (SQLException e) { + throw new IllegalStateException("Could not check if SQLite migration is required", e); + } + } + + private static boolean isMigrationForced(List arguments) { + return arguments.size() >= 2 && "force".equals(arguments.get(1)); + } + + // Cannot rename or remove a column from SQLite, so we have to rename the table and create an updated one + // cf. https://stackoverflow.com/questions/805363/how-do-i-rename-a-column-in-a-sqlite-database-table + private void recreateDatabaseWithNewDefinitions(String tableName, Columns col) throws SQLException { + Connection connection = getConnection(sqLite); + String tempTable = "tmp_" + tableName; + try (Statement st = connection.createStatement()) { + st.execute("ALTER TABLE " + tableName + " RENAME TO " + tempTable + ";"); + } + + sqLite.reload(); + connection = getConnection(sqLite); + + try (Statement st = connection.createStatement()) { + String copySql = "INSERT INTO $table ($id, $name, $realName, $password, $lastIp, $lastLogin, $regIp, " + + "$regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw, $email, $isLogged)" + + "SELECT $id, $name, $realName," + + " $password, CASE WHEN $lastIp = '127.0.0.1' OR $lastIp = '' THEN NULL else $lastIp END," + + " $lastLogin, $regIp, $regDate, $locX, $locY, $locZ, $locWorld, $locPitch, $locYaw," + + " CASE WHEN $email = 'your@email.com' THEN NULL ELSE $email END, $isLogged" + + " FROM " + tempTable + ";"; + int insertedEntries = st.executeUpdate(replaceColumnVariables(copySql, tableName, col)); + ConsoleLogger.info("Copied over " + insertedEntries + " from the old table to the new one"); + + st.execute("DROP TABLE " + tempTable + ";"); + } + } + + private String replaceColumnVariables(String sql, String tableName, Columns col) { + String replacedSql = sql.replace("$table", tableName).replace("$id", col.ID) + .replace("$name", col.NAME).replace("$realName", col.REAL_NAME) + .replace("$password", col.PASSWORD).replace("$lastIp", col.LAST_IP) + .replace("$lastLogin", col.LAST_LOGIN).replace("$regIp", col.REGISTRATION_IP) + .replace("$regDate", col.REGISTRATION_DATE).replace("$locX", col.LASTLOC_X) + .replace("$locY", col.LASTLOC_Y).replace("$locZ", col.LASTLOC_Z) + .replace("$locWorld", col.LASTLOC_WORLD).replace("$locPitch", col.LASTLOC_PITCH) + .replace("$locYaw", col.LASTLOC_YAW).replace("$email", col.EMAIL) + .replace("$isLogged", col.IS_LOGGED); + if (replacedSql.contains("$")) { + throw new IllegalStateException("SQL still statement still has '$' in it - was a tag not replaced?" + + " Replacement result: " + replacedSql); + } + return replacedSql; + } + + /** + * Returns the connection from the given SQLite instance. + * + * @param sqLite the SQLite instance to process + * @return the connection to the SQLite database + */ + private static Connection getConnection(SQLite sqLite) { + try { + Field connectionField = SQLite.class.getDeclaredField("con"); + connectionField.setAccessible(true); + return (Connection) connectionField.get(sqLite); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new IllegalStateException("Failed to get the connection from SQLite", e); + } + } +} diff --git a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java index 13fe08dc..4c8b8ee3 100644 --- a/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/data/auth/PlayerAuth.java @@ -18,6 +18,8 @@ public class PlayerAuth { public static final String DB_EMAIL_DEFAULT = "your@email.com"; /** Default last login value used in the database if the last login column is NOT NULL. */ public static final long DB_LAST_LOGIN_DEFAULT = 0; + /** Default last ip value used in the database if the last IP column is NOT NULL. */ + public static final String DB_LAST_IP_DEFAULT = "127.0.0.1"; /** The player's name in lowercase, e.g. "xephi". */ private String nickname; @@ -218,7 +220,7 @@ public class PlayerAuth { auth.realName = firstNonNull(realName, "Player"); auth.password = firstNonNull(password, new HashedPassword("")); auth.email = DB_EMAIL_DEFAULT.equals(email) ? null : email; - auth.lastIp = firstNonNull(lastIp, "127.0.0.1"); + auth.lastIp = lastIp; // Don't check against default value 127.0.0.1 as it may be a legit value auth.groupId = groupId; auth.lastLogin = isEqualTo(lastLogin, DB_LAST_LOGIN_DEFAULT) ? null : lastLogin; auth.registrationIp = registrationIp; diff --git a/src/main/java/fr/xephi/authme/datasource/MySQL.java b/src/main/java/fr/xephi/authme/datasource/MySQL.java index 6528128a..45240a1a 100644 --- a/src/main/java/fr/xephi/authme/datasource/MySQL.java +++ b/src/main/java/fr/xephi/authme/datasource/MySQL.java @@ -192,7 +192,7 @@ public class MySQL implements DataSource { if (isColumnMissing(md, col.LAST_IP)) { st.executeUpdate("ALTER TABLE " + tableName - + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL;"); + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin;"); } if (isColumnMissing(md, col.LAST_LOGIN)) { diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index acf050eb..7497858f 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -97,7 +97,7 @@ public class SQLite implements DataSource { if (isColumnMissing(md, col.LAST_IP)) { st.executeUpdate("ALTER TABLE " + tableName - + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40) NOT NULL DEFAULT '';"); + + " ADD COLUMN " + col.LAST_IP + " VARCHAR(40);"); } if (isColumnMissing(md, col.LAST_LOGIN)) { @@ -152,10 +152,30 @@ public class SQLite implements DataSource { st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.HAS_SESSION + " INT NOT NULL DEFAULT '0';"); } + + if (isMigrationRequired(md)) { + ConsoleLogger.warning("READ ME! Your SQLite database is outdated and cannot save new players."); + ConsoleLogger.warning("Run /authme debug migratesqlite after making a backup"); + } } ConsoleLogger.info("SQLite Setup finished"); } + /** + * Returns whether the database needs to be migrated. + *

+ * Background: Before commit 22911a0 (July 2016), new SQLite databases initialized the last IP column to be NOT NULL + * without a default value. Allowing the last IP to be null (#792) is therefore not compatible. + * + * @param metaData the database meta data + * @return true if a migration is necessary, false otherwise + * @throws SQLException . + */ + public boolean isMigrationRequired(DatabaseMetaData metaData) throws SQLException { + return SqlDataSourceUtils.isNotNullColumn(metaData, tableName, col.LAST_IP) + && SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, col.LAST_IP) == null; + } + private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException { try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { return !rs.next(); diff --git a/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java b/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java index c84505d8..da5a6ca2 100644 --- a/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java +++ b/src/main/java/fr/xephi/authme/datasource/SqlDataSourceUtils.java @@ -2,13 +2,14 @@ package fr.xephi.authme.datasource; import fr.xephi.authme.ConsoleLogger; +import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; /** * Utilities for SQL data sources. */ -final class SqlDataSourceUtils { +public final class SqlDataSourceUtils { private SqlDataSourceUtils() { } @@ -18,7 +19,7 @@ final class SqlDataSourceUtils { * * @param e the exception to log */ - static void logSqlException(SQLException e) { + public static void logSqlException(SQLException e) { ConsoleLogger.logException("Error during SQL operation:", e); } @@ -31,8 +32,55 @@ final class SqlDataSourceUtils { * @return the value (which may be null) * @throws SQLException :) */ - static Long getNullableLong(ResultSet rs, String columnName) throws SQLException { + public static Long getNullableLong(ResultSet rs, String columnName) throws SQLException { long longValue = rs.getLong(columnName); return rs.wasNull() ? null : longValue; } + + /** + * Returns whether the given column has a NOT NULL constraint. + * + * @param metaData the database meta data + * @param tableName the name of the table in which the column is + * @param columnName the name of the column to check + * @return true if the column is NOT NULL, false otherwise + * @throws SQLException :) + */ + public static boolean isNotNullColumn(DatabaseMetaData metaData, String tableName, + String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + if (!rs.next()) { + throw new IllegalStateException("Did not find meta data for column '" + + columnName + "' while checking for not-null constraint"); + } + + int nullableCode = rs.getInt("NULLABLE"); + if (nullableCode == DatabaseMetaData.columnNoNulls) { + return true; + } else if (nullableCode == DatabaseMetaData.columnNullableUnknown) { + ConsoleLogger.warning("Unknown nullable status for column '" + columnName + "'"); + } + } + return false; + } + + /** + * Returns the default value of a column (as per its SQL definition). + * + * @param metaData the database meta data + * @param tableName the name of the table in which the column is + * @param columnName the name of the column to check + * @return the default value of the column (may be null) + * @throws SQLException :) + */ + public static Object getColumnDefaultValue(DatabaseMetaData metaData, String tableName, + String columnName) throws SQLException { + try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) { + if (!rs.next()) { + throw new IllegalStateException("Did not find meta data for column '" + + columnName + "' while checking its default value"); + } + return rs.getObject("COLUMN_DEF"); + } + } } diff --git a/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java b/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java index 6461c29a..5bcdb257 100644 --- a/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java +++ b/src/main/java/fr/xephi/authme/permission/DebugSectionPermissions.java @@ -32,6 +32,9 @@ public enum DebugSectionPermissions implements PermissionNode { /** Permission to change nullable status of MySQL columns. */ MYSQL_DEFAULT_CHANGER("authme.debug.mysqldef"), + /** Permission to perform a migration of SQLite. */ + MIGRATE_SQLITE("authme.debug.migratesqlite"), + /** Permission to view spawn information. */ SPAWN_LOCATION("authme.debug.spawn"), diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 9f24120b..2b227193 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -173,6 +173,7 @@ permissions: authme.debug.group: true authme.debug.limbo: true authme.debug.mail: true + authme.debug.migratesqlite: true authme.debug.mysqldef: true authme.debug.perm: true authme.debug.spawn: true @@ -196,6 +197,9 @@ permissions: authme.debug.mail: description: Permission to use the test email sender. default: op + authme.debug.migratesqlite: + description: Permission to perform a migration of SQLite. + default: op authme.debug.mysqldef: description: Permission to change nullable status of MySQL columns. default: op diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java index 03256d7a..400c7c07 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/AccountsCommandTest.java @@ -82,7 +82,8 @@ public class AccountsCommandTest { // given CommandSender sender = mock(CommandSender.class); List arguments = Collections.singletonList("SomeUser"); - given(dataSource.getAuth("someuser")).willReturn(mock(PlayerAuth.class)); + PlayerAuth auth = authWithIp("144.56.77.88"); + given(dataSource.getAuth("someuser")).willReturn(auth); // when command.executeCommand(sender, arguments); diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java index c0e35b80..ea92f85e 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionConsistencyTest.java @@ -11,6 +11,8 @@ import java.util.List; import java.util.Set; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; @@ -20,11 +22,16 @@ import static org.junit.Assert.fail; public class DebugSectionConsistencyTest { private static List> debugClasses; + private static List debugSections; @BeforeClass public static void collectClasses() { - debugClasses = new ClassCollector( - TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT + "command/executable/authme/debug").collectClasses(); + // TODO ljacqu 20171021: Improve ClassCollector (pass pkg by class, improve #getInstancesOfType's instantiation) + ClassCollector classCollector = new ClassCollector( + TestHelper.SOURCES_FOLDER, TestHelper.PROJECT_ROOT + "command/executable/authme/debug"); + + debugClasses = classCollector.collectClasses(); + debugSections = classCollector.getInstancesOfType(DebugSection.class, clz -> instantiate(clz)); } @Test @@ -40,13 +47,26 @@ public class DebugSectionConsistencyTest { @Test public void shouldHaveDifferentSubcommandName() throws IllegalAccessException, InstantiationException { Set names = new HashSet<>(); - for (Class clazz : debugClasses) { - if (DebugSection.class.isAssignableFrom(clazz) && !clazz.isInterface()) { - DebugSection debugSection = (DebugSection) clazz.newInstance(); - if (!names.add(debugSection.getName())) { - fail("Encountered name '" + debugSection.getName() + "' a second time in " + clazz); - } + for (DebugSection debugSection : debugSections) { + if (!names.add(debugSection.getName())) { + fail("Encountered name '" + debugSection.getName() + "' a second time in " + debugSection.getClass()); } } } + + @Test + public void shouldAllHaveDescription() { + for (DebugSection debugSection : debugSections) { + assertThat("Description of '" + debugSection.getClass() + "' may not be null", + debugSection.getDescription(), not(nullValue())); + } + } + + private static DebugSection instantiate(Class clazz) { + try { + return ClassCollector.canInstantiate(clazz) ? clazz.newInstance() : null; + } catch (InstantiationException | IllegalAccessException e) { + throw new IllegalStateException(e); + } + } } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java index bd4b42e0..aadcfa40 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/DebugSectionUtilsTest.java @@ -2,8 +2,11 @@ package fr.xephi.authme.command.executable.authme.debug; import fr.xephi.authme.ReflectionTestUtils; import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.data.limbo.LimboPlayer; import fr.xephi.authme.data.limbo.LimboService; +import fr.xephi.authme.datasource.CacheDataSource; +import fr.xephi.authme.datasource.DataSource; import org.bukkit.Location; import org.junit.Before; import org.junit.Test; @@ -89,4 +92,39 @@ public class DebugSectionUtilsTest { // then assertThat(result, nullValue()); } + + @Test + public void shouldReturnSameDataSourceInstance() { + // given + DataSource dataSource = mock(DataSource.class); + + // when + DataSource result = DebugSectionUtils.unwrapSourceFromCacheDataSource(dataSource); + + // then + assertThat(result, equalTo(dataSource)); + } + + @Test + public void shouldUnwrapCacheDataSource() { + // given + DataSource source = mock(DataSource.class); + PlayerCache playerCache = mock(PlayerCache.class); + CacheDataSource cacheDataSource = new CacheDataSource(source, playerCache); + + // when + DataSource result = DebugSectionUtils.unwrapSourceFromCacheDataSource(cacheDataSource); + + // then + assertThat(result, equalTo(source)); + } + + @Test + public void shouldCastOrReturnNull() { + // given / when / then + assertThat(DebugSectionUtils.castToTypeOrNull("test", String.class), equalTo("test")); + assertThat(DebugSectionUtils.castToTypeOrNull("test", Integer.class), nullValue()); + assertThat(DebugSectionUtils.castToTypeOrNull(5, String.class), nullValue()); + assertThat(DebugSectionUtils.castToTypeOrNull(5, Integer.class), equalTo(5)); + } } diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChangerTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChangerTest.java index c40f5a99..d4320422 100644 --- a/src/test/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChangerTest.java +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/MySqlDefaultChangerTest.java @@ -7,7 +7,7 @@ import fr.xephi.authme.data.auth.PlayerCache; import fr.xephi.authme.datasource.CacheDataSource; import fr.xephi.authme.datasource.DataSource; import fr.xephi.authme.datasource.MySQL; -import fr.xephi.authme.datasource.MySqlTestUtil; +import fr.xephi.authme.datasource.SqlDataSourceTestUtil; import fr.xephi.authme.settings.Settings; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,32 +34,6 @@ public class MySqlDefaultChangerTest { @Mock private Settings settings; - @Test - public void shouldReturnSameDataSourceInstance() { - // given - DataSource dataSource = mock(DataSource.class); - - // when - DataSource result = MySqlDefaultChanger.unwrapSourceFromCacheDataSource(dataSource); - - // then - assertThat(result, equalTo(dataSource)); - } - - @Test - public void shouldUnwrapCacheDataSource() { - // given - DataSource source = mock(DataSource.class); - PlayerCache playerCache = mock(PlayerCache.class); - CacheDataSource cacheDataSource = new CacheDataSource(source, playerCache); - - // when - DataSource result = MySqlDefaultChanger.unwrapSourceFromCacheDataSource(cacheDataSource); - - // then - assertThat(result, equalTo(source)); - } - @Test public void shouldReturnMySqlConnection() throws SQLException { // given @@ -68,7 +42,7 @@ public class MySqlDefaultChangerTest { HikariDataSource dataSource = mock(HikariDataSource.class); Connection connection = mock(Connection.class); given(dataSource.getConnection()).willReturn(connection); - MySQL mySQL = MySqlTestUtil.createMySql(settings, dataSource); + MySQL mySQL = SqlDataSourceTestUtil.createMySql(settings, dataSource); MySqlDefaultChanger defaultChanger = createDefaultChanger(mySQL); // when diff --git a/src/test/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigraterIntegrationTest.java b/src/test/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigraterIntegrationTest.java new file mode 100644 index 00000000..caa0b5c8 --- /dev/null +++ b/src/test/java/fr/xephi/authme/command/executable/authme/debug/SqliteMigraterIntegrationTest.java @@ -0,0 +1,109 @@ +package fr.xephi.authme.command.executable.authme.debug; + +import com.google.common.collect.Lists; +import com.google.common.io.Files; +import fr.xephi.authme.ReflectionTestUtils; +import fr.xephi.authme.TestHelper; +import fr.xephi.authme.data.auth.PlayerAuth; +import fr.xephi.authme.datasource.SQLite; +import fr.xephi.authme.settings.Settings; +import org.bukkit.command.CommandSender; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +import static fr.xephi.authme.AuthMeMatchers.hasAuthBasicData; +import static fr.xephi.authme.AuthMeMatchers.hasAuthLocation; +import static fr.xephi.authme.datasource.SqlDataSourceTestUtil.createSqliteAndInitialize; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Integration test for {@link SqliteMigrater}. Uses a real SQLite database. + */ +public class SqliteMigraterIntegrationTest { + + private static final String CONFIRMATION_CODE = "ABCD"; + + private SqliteMigrater sqliteMigrater; + private SQLite sqLite; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setup() throws SQLException, IOException, NoSuchMethodException { + TestHelper.setupLogger(); + + Settings settings = mock(Settings.class); + TestHelper.returnDefaultsForAllProperties(settings); + + File sqliteDbFile = TestHelper.getJarFile(TestHelper.PROJECT_ROOT + "datasource/sqlite.april2016.db"); + File tempFile = temporaryFolder.newFile(); + Files.copy(sqliteDbFile, tempFile); + + Connection con = DriverManager.getConnection("jdbc:sqlite:" + tempFile.getPath()); + sqLite = createSqliteAndInitialize(settings, con); + + sqliteMigrater = new SqliteMigrater(); + ReflectionTestUtils.setField(sqliteMigrater, "dataSource", sqLite); + ReflectionTestUtils.setField(sqliteMigrater, "settings", settings); + ReflectionTestUtils.setField(sqliteMigrater, "confirmationCode", CONFIRMATION_CODE); + sqliteMigrater.setSqLiteField(); + } + + @Test + public void shouldRun() throws ClassNotFoundException, SQLException { + // given + CommandSender sender = mock(CommandSender.class); + + // when + sqliteMigrater.execute(sender, Collections.singletonList(CONFIRMATION_CODE)); + + // then + List auths = sqLite.getAllAuths(); + assertThat(Lists.transform(auths, PlayerAuth::getNickname), + containsInAnyOrder("mysql1", "mysql2", "mysql3", "mysql4", "mysql5", "mysql6")); + PlayerAuth auth1 = getByNameOrFail("mysql1", auths); + assertThat(auth1, hasAuthBasicData("mysql1", "mysql1", "user1@example.com", "192.168.4.41")); + assertThat(auth1, hasAuthLocation(0, 0, 0, "world1", 0, 0)); + assertThat(auth1.getLastLogin(), equalTo(1472992664137L)); + PlayerAuth auth2 = getByNameOrFail("mysql2", auths); + assertThat(auth2, hasAuthBasicData("mysql2", "Player", "user2@example.com", null)); + assertThat(auth2, hasAuthLocation(0, 0, 0, "world2", 0, 0)); + assertThat(auth2.getLastLogin(), equalTo(1472992668391L)); + PlayerAuth auth3 = getByNameOrFail("mysql3", auths); + assertThat(auth3, hasAuthBasicData("mysql3", "mysql3", null, "132.54.76.98")); + assertThat(auth3, hasAuthLocation(0, 0, 0, "world3", 0, 0)); + assertThat(auth3.getLastLogin(), equalTo(1472992672790L)); + PlayerAuth auth4 = getByNameOrFail("mysql4", auths); + assertThat(auth4, hasAuthBasicData("mysql4", "MySQL4", null, null)); + assertThat(auth4, hasAuthLocation(25, 4, 17, "world4", 0, 0)); + assertThat(auth4.getLastLogin(), equalTo(1472992676790L)); + PlayerAuth auth5 = getByNameOrFail("mysql5", auths); + assertThat(auth5, hasAuthBasicData("mysql5", "mysql5", null, null)); + assertThat(auth5, hasAuthLocation(0, 0, 0, "world5", 0, 0)); + assertThat(auth5.getLastLogin(), equalTo(1472992680922L)); + PlayerAuth auth6 = getByNameOrFail("mysql6", auths); + assertThat(auth6, hasAuthBasicData("mysql6", "MySql6", "user6@example.com", "44.45.67.188")); + assertThat(auth6, hasAuthLocation(28.5, 53.43, -147.23, "world6", 0, 0)); + assertThat(auth6.getLastLogin(), equalTo(1472992686300L)); + } + + private static PlayerAuth getByNameOrFail(String name, List auths) { + return auths.stream() + .filter(auth -> name.equals(auth.getNickname())) + .findFirst().orElseThrow(() -> new IllegalStateException("No PlayerAuth with name '" + name + "'")); + } +} diff --git a/src/test/java/fr/xephi/authme/data/auth/PlayerAuthTest.java b/src/test/java/fr/xephi/authme/data/auth/PlayerAuthTest.java index 65a8ada8..360abdc9 100644 --- a/src/test/java/fr/xephi/authme/data/auth/PlayerAuthTest.java +++ b/src/test/java/fr/xephi/authme/data/auth/PlayerAuthTest.java @@ -18,12 +18,16 @@ public class PlayerAuthTest { PlayerAuth auth = PlayerAuth.builder() .name("Bobby") .lastLogin(0L) + .lastIp("127.0.0.1") .email("your@email.com") .build(); // then assertThat(auth.getNickname(), equalTo("bobby")); assertThat(auth.getLastLogin(), nullValue()); + // Note ljacqu 20171020: Although 127.0.0.1 is the default value, we need to keep it because it might + // legitimately be the resolved IP of a player + assertThat(auth.getLastIp(), equalTo("127.0.0.1")); assertThat(auth.getEmail(), nullValue()); } @@ -50,6 +54,7 @@ public class PlayerAuthTest { .name("Charlie") .email(null) .lastLogin(null) + .lastIp(null) .groupId(19) .locPitch(123.004f) .build(); @@ -57,6 +62,7 @@ public class PlayerAuthTest { // then assertThat(auth.getEmail(), nullValue()); assertThat(auth.getLastLogin(), nullValue()); + assertThat(auth.getLastIp(), nullValue()); assertThat(auth.getGroupId(), equalTo(19)); assertThat(auth.getPitch(), equalTo(123.004f)); } diff --git a/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java index 3fcddcc4..9732ced7 100644 --- a/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/FlatFileIntegrationTest.java @@ -64,7 +64,7 @@ public class FlatFileIntegrationTest { assertThat(getName("bobby", authList), hasAuthBasicData("bobby", "bobby", null, "123.45.67.89")); assertThat(getName("bobby", authList), hasAuthLocation(1.05, 2.1, 4.2, "world", 0, 0)); assertThat(getName("bobby", authList).getPassword(), equalToHash("$SHA$11aa0706173d7272$dbba966")); - assertThat(getName("twofields", authList), hasAuthBasicData("twofields", "twofields", null, "127.0.0.1")); + assertThat(getName("twofields", authList), hasAuthBasicData("twofields", "twofields", null, null)); assertThat(getName("twofields", authList).getPassword(), equalToHash("hash1234")); assertThat(getName("threefields", authList), hasAuthBasicData("threefields", "threefields", null, "33.33.33.33")); assertThat(getName("fourfields", authList), hasAuthBasicData("fourfields", "fourfields", null, "4.4.4.4")); diff --git a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java index 75be337d..eab6a4cc 100644 --- a/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java +++ b/src/test/java/fr/xephi/authme/datasource/MySqlIntegrationTest.java @@ -76,7 +76,7 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest { @Override protected DataSource getDataSource(String saltColumn) { when(settings.getProperty(DatabaseSettings.MYSQL_COL_SALT)).thenReturn(saltColumn); - return MySqlTestUtil.createMySql(settings, hikariSource); + return SqlDataSourceTestUtil.createMySql(settings, hikariSource); } private static void set(Property property, T value) { diff --git a/src/test/java/fr/xephi/authme/datasource/MySqlTestUtil.java b/src/test/java/fr/xephi/authme/datasource/MySqlTestUtil.java deleted file mode 100644 index 597a7eec..00000000 --- a/src/test/java/fr/xephi/authme/datasource/MySqlTestUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.xephi.authme.datasource; - -import com.zaxxer.hikari.HikariDataSource; -import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; -import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; -import fr.xephi.authme.settings.Settings; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Test util for the MySQL data source. - */ -public final class MySqlTestUtil { - - private MySqlTestUtil() { - } - - public static MySQL createMySql(Settings settings, HikariDataSource hikariDataSource) { - MySqlExtensionsFactory extensionsFactory = mock(MySqlExtensionsFactory.class); - given(extensionsFactory.buildExtension(any())).willReturn(mock(MySqlExtension.class)); - return createMySql(settings, hikariDataSource, extensionsFactory); - } - - public static MySQL createMySql(Settings settings, HikariDataSource hikariDataSource, - MySqlExtensionsFactory extensionsFactory) { - return new MySQL(settings, hikariDataSource, extensionsFactory); - } -} diff --git a/src/test/java/fr/xephi/authme/datasource/SqlDataSourceTestUtil.java b/src/test/java/fr/xephi/authme/datasource/SqlDataSourceTestUtil.java new file mode 100644 index 00000000..13084350 --- /dev/null +++ b/src/test/java/fr/xephi/authme/datasource/SqlDataSourceTestUtil.java @@ -0,0 +1,52 @@ +package fr.xephi.authme.datasource; + +import com.zaxxer.hikari.HikariDataSource; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtension; +import fr.xephi.authme.datasource.mysqlextensions.MySqlExtensionsFactory; +import fr.xephi.authme.settings.Settings; + +import java.sql.Connection; +import java.sql.SQLException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Test util for the SQL data sources. + */ +public final class SqlDataSourceTestUtil { + + private SqlDataSourceTestUtil() { + } + + public static MySQL createMySql(Settings settings, HikariDataSource hikariDataSource) { + MySqlExtensionsFactory extensionsFactory = mock(MySqlExtensionsFactory.class); + given(extensionsFactory.buildExtension(any())).willReturn(mock(MySqlExtension.class)); + return new MySQL(settings, hikariDataSource, extensionsFactory); + } + + public static SQLite createSqlite(Settings settings, Connection connection) { + return new SQLite(settings, connection) { + // Override reload() so it doesn't run SQLite#connect, since we're given a specific Connection to use + @Override + public void reload() { + try { + this.setup(); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + }; + } + + public static SQLite createSqliteAndInitialize(Settings settings, Connection connection) { + SQLite sqLite = createSqlite(settings, connection); + try { + sqLite.setup(); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + return sqLite; + } +} diff --git a/src/test/java/fr/xephi/authme/datasource/SqlDataSourceUtilsTest.java b/src/test/java/fr/xephi/authme/datasource/SqlDataSourceUtilsTest.java index ca070f59..e196fabd 100644 --- a/src/test/java/fr/xephi/authme/datasource/SqlDataSourceUtilsTest.java +++ b/src/test/java/fr/xephi/authme/datasource/SqlDataSourceUtilsTest.java @@ -4,10 +4,17 @@ import fr.xephi.authme.TestHelper; import org.junit.Before; import org.junit.Test; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.logging.Logger; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.hamcrest.MockitoHamcrest.argThat; @@ -40,4 +47,108 @@ public class SqlDataSourceUtilsTest { // then verify(logger).warning(argThat(containsString(msg))); } + + @Test + public void shouldFetchNullableStatus() throws SQLException { + // given + String tableName = "data"; + String columnName = "category"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.getInt("NULLABLE")).willReturn(DatabaseMetaData.columnNullable); + given(resultSet.next()).willReturn(true); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + boolean result = SqlDataSourceUtils.isNotNullColumn(metaData, tableName, columnName); + + // then + assertThat(result, equalTo(false)); + } + + @Test + public void shouldReturnFalseForUnknownNullableStatus() throws SQLException { + // given + String tableName = "comments"; + String columnName = "author"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.getInt("NULLABLE")).willReturn(DatabaseMetaData.columnNullableUnknown); + given(resultSet.next()).willReturn(true); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + boolean result = SqlDataSourceUtils.isNotNullColumn(metaData, tableName, columnName); + + // then + assertThat(result, equalTo(false)); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowForUnknownColumnInNullableCheck() throws SQLException { + // given + String tableName = "data"; + String columnName = "unknown"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(false); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + SqlDataSourceUtils.isNotNullColumn(metaData, tableName, columnName); + + // then - expect exception + } + + @Test + public void shouldGetDefaultValue() throws SQLException { + // given + String tableName = "data"; + String columnName = "category"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.getObject("COLUMN_DEF")).willReturn("Literature"); + given(resultSet.next()).willReturn(true); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + Object defaultValue = SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, columnName); + + // then + assertThat(defaultValue, equalTo("Literature")); + } + + @Test(expected = IllegalStateException.class) + public void shouldThrowForUnknownColumnInDefaultValueRetrieval() throws SQLException { + // given + String tableName = "data"; + String columnName = "unknown"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(false); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, columnName); + + // then - expect exception + } + + @Test + public void shouldHandleNullDefaultValue() throws SQLException { + // given + String tableName = "data"; + String columnName = "category"; + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.getObject("COLUMN_DEF")).willReturn(null); + given(resultSet.next()).willReturn(true); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(metaData.getColumns(null, null, tableName, columnName)).willReturn(resultSet); + + // when + Object defaultValue = SqlDataSourceUtils.getColumnDefaultValue(metaData, tableName, columnName); + + // then + assertThat(defaultValue, nullValue()); + } } diff --git a/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java b/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java index d3ad34f0..54201fb7 100644 --- a/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java +++ b/src/test/java/fr/xephi/authme/datasource/converter/LoginSecurityConverterTest.java @@ -29,6 +29,7 @@ import static fr.xephi.authme.AuthMeMatchers.hasAuthLocation; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -75,7 +76,7 @@ public class LoginSecurityConverterTest { assertThat(captor.getAllValues().get(0).getLastLogin(), equalTo(1494242093652L)); assertThat(captor.getAllValues().get(0).getRegistrationDate(), equalTo(1494242093400L)); assertThat(captor.getAllValues().get(0).getPassword(), equalToHash("$2a$10$E1Ri7XKeIIBv4qVaiPplgepT7QH9xGFh3hbHfcmCjq7hiW.UBTiGK")); - assertThat(captor.getAllValues().get(0).getLastIp(), equalTo("127.0.0.1")); + assertThat(captor.getAllValues().get(0).getLastIp(), nullValue()); assertThat(captor.getAllValues().get(1).getNickname(), equalTo("player2")); assertThat(captor.getAllValues().get(1).getLastLogin(), equalTo(1494242174589L)); @@ -84,7 +85,7 @@ public class LoginSecurityConverterTest { assertThat(captor.getAllValues().get(2).getRealName(), equalTo("Player3")); assertThat(captor.getAllValues().get(2).getPassword(), equalToHash("$2a$10$WFui8KSXMLDOVXKFpCLyPukPi4M82w1cv/rNojsAnwJjba3pp8sba")); assertThat(captor.getAllValues().get(2), hasAuthLocation(14.24, 67.99, -12.83, "hubb", -10f, 185f)); - assertThat(captor.getAllValues().get(2).getLastIp(), equalTo("127.0.0.1")); + assertThat(captor.getAllValues().get(2).getLastIp(), nullValue()); assertIsCloseTo(captor.getAllValues().get(2).getRegistrationDate(), System.currentTimeMillis(), 500L); } @@ -108,7 +109,7 @@ public class LoginSecurityConverterTest { assertThat(captor.getAllValues().get(0).getRealName(), equalTo("Player1")); assertThat(captor.getAllValues().get(0).getLastLogin(), equalTo(1494242093000L)); assertThat(captor.getAllValues().get(0).getPassword(), equalToHash("$2a$10$E1Ri7XKeIIBv4qVaiPplgepT7QH9xGFh3hbHfcmCjq7hiW.UBTiGK")); - assertThat(captor.getAllValues().get(0).getLastIp(), equalTo("127.0.0.1")); + assertThat(captor.getAllValues().get(0).getLastIp(), nullValue()); assertIsCloseTo(captor.getAllValues().get(0).getRegistrationDate(), 1494201600000L, 12 * 60 * 60 * 1000); assertThat(captor.getAllValues().get(1).getNickname(), equalTo("player2")); diff --git a/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java b/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java index 891d4271..a04aa713 100644 --- a/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java +++ b/src/test/java/fr/xephi/authme/process/register/executors/EmailRegisterExecutorProviderTest.java @@ -121,7 +121,7 @@ public class EmailRegisterExecutorProviderTest { PlayerAuth auth = executor.buildPlayerAuth(params); // then - assertThat(auth, hasAuthBasicData("veronica", "Veronica", "test@example.com", "127.0.0.1")); + assertThat(auth, hasAuthBasicData("veronica", "Veronica", "test@example.com", null)); assertThat(auth.getRegistrationIp(), equalTo("123.45.67.89")); assertIsCloseTo(auth.getRegistrationDate(), System.currentTimeMillis(), 1000); assertThat(auth.getPassword().getHash(), stringWithLength(12)); diff --git a/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java b/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java index cf471e47..66ebb7c2 100644 --- a/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java +++ b/src/test/java/fr/xephi/authme/process/register/executors/PasswordRegisterExecutorTest.java @@ -103,7 +103,7 @@ public class PasswordRegisterExecutorTest { PlayerAuth auth = executor.buildPlayerAuth(params); // then - assertThat(auth, hasAuthBasicData("s1m0n", "S1m0N", "mail@example.org", "127.0.0.1")); + assertThat(auth, hasAuthBasicData("s1m0n", "S1m0N", "mail@example.org", null)); assertThat(auth.getRegistrationIp(), equalTo("123.45.67.89")); assertIsCloseTo(auth.getRegistrationDate(), System.currentTimeMillis(), 500); assertThat(auth.getPassword(), equalToHash("pass")); diff --git a/src/test/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelperTest.java b/src/test/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelperTest.java index 0ac7c064..a5d583df 100644 --- a/src/test/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelperTest.java +++ b/src/test/java/fr/xephi/authme/process/register/executors/PlayerAuthBuilderHelperTest.java @@ -33,7 +33,7 @@ public class PlayerAuthBuilderHelperTest { PlayerAuth auth = PlayerAuthBuilderHelper.createPlayerAuth(player, hashedPassword, email); // then - assertThat(auth, hasAuthBasicData("noah", "Noah", email, "127.0.0.1")); + assertThat(auth, hasAuthBasicData("noah", "Noah", email, null)); assertThat(auth.getRegistrationIp(), equalTo("192.168.34.47")); assertThat(Math.abs(auth.getRegistrationDate() - System.currentTimeMillis()), lessThan(1000L)); assertThat(auth.getPassword(), equalToHash("myHash0001")); diff --git a/src/test/java/fr/xephi/authme/service/SessionServiceTest.java b/src/test/java/fr/xephi/authme/service/SessionServiceTest.java index f67326a6..754487c1 100644 --- a/src/test/java/fr/xephi/authme/service/SessionServiceTest.java +++ b/src/test/java/fr/xephi/authme/service/SessionServiceTest.java @@ -213,6 +213,30 @@ public class SessionServiceTest { verify(dataSource).getAuth(name); } + @Test + public void shouldHandlePlayerAuthWithNullLastIp() { + // given + String name = "Charles"; + Player player = mockPlayerWithNameAndIp(name, "144.117.118.145"); + given(dataSource.hasSession(name)).willReturn(true); + PlayerAuth auth = PlayerAuth.builder() + .name(name) + .lastIp(null) + .lastLogin(System.currentTimeMillis()).build(); + given(dataSource.getAuth(name)).willReturn(auth); + + // when + boolean result = sessionService.canResumeSession(player); + + // then + assertThat(result, equalTo(false)); + verify(commonService).getProperty(PluginSettings.SESSIONS_ENABLED); + verify(dataSource).hasSession(name); + verify(dataSource).setUnlogged(name); + verify(dataSource).revokeSession(name); + verify(dataSource).getAuth(name); + } + private static Player mockPlayerWithNameAndIp(String name, String ip) { Player player = mock(Player.class); given(player.getName()).willReturn(name); diff --git a/src/test/resources/fr/xephi/authme/datasource/sqlite.april2016.db b/src/test/resources/fr/xephi/authme/datasource/sqlite.april2016.db new file mode 100644 index 00000000..8973f01d Binary files /dev/null and b/src/test/resources/fr/xephi/authme/datasource/sqlite.april2016.db differ