Remove duplicate column initialization code, add datasource integration tests

- Drop initialization of all columns on table create in favor of checking each column individually. This is slower but guarantees that each column is only defined once in the code. Columns are only created once so having clean code outweighs performance.
- Write more datasource integration tests
This commit is contained in:
ljacqu 2016-07-02 19:16:26 +02:00
parent f1d5f3df28
commit 22911a0bb9
6 changed files with 185 additions and 81 deletions

View File

@ -3,14 +3,12 @@ package fr.xephi.authme.datasource;
import com.google.common.annotations.VisibleForTesting;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.security.HashAlgorithm;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.security.crypts.XFBCRYPT;
import fr.xephi.authme.settings.NewSetting;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.settings.properties.HooksSettings;
import fr.xephi.authme.settings.properties.SecuritySettings;
@ -69,7 +67,7 @@ public class MySQL implements DataSource {
// Initialize the database
try {
setupConnection();
checkTablesAndColumns();
} catch (SQLException e) {
close();
ConsoleLogger.logException("Can't initialize the MySQL database:", e);
@ -140,24 +138,13 @@ public class MySQL implements DataSource {
return ds.getConnection();
}
private void setupConnection() throws SQLException {
private void checkTablesAndColumns() throws SQLException {
try (Connection con = getConnection(); Statement st = con.createStatement()) {
// Create table if not exists.
// Create table with ID column if it doesn't exist
String sql = "CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ col.ID + " MEDIUMINT(8) UNSIGNED AUTO_INCREMENT,"
+ col.NAME + " VARCHAR(255) NOT NULL UNIQUE,"
+ col.REAL_NAME + " VARCHAR(255) NOT NULL,"
+ col.PASSWORD + " VARCHAR(255) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,"
+ col.IP + " VARCHAR(40) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '127.0.0.1',"
+ col.LAST_LOGIN + " BIGINT NOT NULL DEFAULT 0,"
+ col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0',"
+ col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0',"
+ col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0',"
+ col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT '" + Settings.defaultWorld + "',"
+ col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com',"
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0',"
+ "PRIMARY KEY (" + col.ID + ")"
+ ") CHARACTER SET = utf8";
+ ") CHARACTER SET = utf8;";
st.executeUpdate(sql);
DatabaseMetaData md = con.getMetaData();
@ -177,8 +164,7 @@ public class MySQL implements DataSource {
}
if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.SALT + " VARCHAR(255);");
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
if (isColumnMissing(md, col.IP)) {
@ -219,8 +205,6 @@ public class MySQL implements DataSource {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.IS_LOGGED + " SMALLINT NOT NULL DEFAULT '0' AFTER " + col.EMAIL);
}
st.close();
}
ConsoleLogger.info("MySQL setup finished");
}

View File

@ -5,11 +5,11 @@ import fr.xephi.authme.ConsoleLogger;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.security.crypts.HashedPassword;
import fr.xephi.authme.settings.NewSetting;
import fr.xephi.authme.settings.Settings;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import fr.xephi.authme.util.StringUtils;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
@ -69,67 +69,73 @@ public class SQLite implements DataSource {
this.con = DriverManager.getConnection("jdbc:sqlite:plugins/AuthMe/" + database + ".db");
}
private void setup() throws SQLException {
Statement st = null;
ResultSet rs = null;
try {
st = con.createStatement();
st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " (" + col.ID + " INTEGER AUTO_INCREMENT," + col.NAME + " VARCHAR(255) NOT NULL UNIQUE," + col.PASSWORD + " VARCHAR(255) NOT NULL," + col.IP + " VARCHAR(40) NOT NULL," + col.LAST_LOGIN + " BIGINT," + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0'," + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT '" + Settings.defaultWorld + "'," + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com'," + "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));");
rs = con.getMetaData().getColumns(null, null, tableName, col.PASSWORD);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL;");
@VisibleForTesting
protected void setup() throws SQLException {
try (Statement st = con.createStatement()) {
// Note: cannot add unique fields later on in SQLite, so we add it on initialization
st.executeUpdate("CREATE TABLE IF NOT EXISTS " + tableName + " ("
+ col.ID + " INTEGER AUTO_INCREMENT, "
+ col.NAME + " VARCHAR(255) NOT NULL UNIQUE, "
+ "CONSTRAINT table_const_prim PRIMARY KEY (" + col.ID + "));");
DatabaseMetaData md = con.getMetaData();
if (isColumnMissing(md, col.REAL_NAME)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN "
+ col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';");
}
rs.close();
if (!col.SALT.isEmpty()) {
rs = con.getMetaData().getColumns(null, null, tableName, col.SALT);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
rs.close();
if (isColumnMissing(md, col.PASSWORD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.PASSWORD + " VARCHAR(255) NOT NULL DEFAULT '';");
}
rs = con.getMetaData().getColumns(null, null, tableName, col.IP);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IP + " VARCHAR(40) NOT NULL;");
if (!col.SALT.isEmpty() && isColumnMissing(md, col.SALT)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.SALT + " VARCHAR(255);");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.LAST_LOGIN);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP DEFAULT current_timestamp;");
if (isColumnMissing(md, col.IP)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.IP + " VARCHAR(40) NOT NULL DEFAULT '';");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.LASTLOC_X);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X + " DOUBLE NOT NULL DEFAULT '0.0';");
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y + " DOUBLE NOT NULL DEFAULT '0.0';");
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z + " DOUBLE NOT NULL DEFAULT '0.0';");
if (isColumnMissing(md, col.LAST_LOGIN)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LAST_LOGIN + " TIMESTAMP;");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.LASTLOC_WORLD);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';");
if (isColumnMissing(md, col.LASTLOC_X)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_X
+ " DOUBLE NOT NULL DEFAULT '0.0';");
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Y
+ " DOUBLE NOT NULL DEFAULT '0.0';");
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.LASTLOC_Z
+ " DOUBLE NOT NULL DEFAULT '0.0';");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.EMAIL);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com';");
if (isColumnMissing(md, col.LASTLOC_WORLD)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.LASTLOC_WORLD + " VARCHAR(255) NOT NULL DEFAULT 'world';");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.IS_LOGGED);
if (!rs.next()) {
if (isColumnMissing(md, col.EMAIL)) {
st.executeUpdate("ALTER TABLE " + tableName
+ " ADD COLUMN " + col.EMAIL + " VARCHAR(255) DEFAULT 'your@email.com';");
}
if (isColumnMissing(md, col.IS_LOGGED)) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.IS_LOGGED + " INT DEFAULT '0';");
}
rs.close();
rs = con.getMetaData().getColumns(null, null, tableName, col.REAL_NAME);
if (!rs.next()) {
st.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + col.REAL_NAME + " VARCHAR(255) NOT NULL DEFAULT 'Player';");
}
} finally {
close(rs);
close(st);
}
ConsoleLogger.info("SQLite Setup finished");
}
private boolean isColumnMissing(DatabaseMetaData metaData, String columnName) throws SQLException {
try (ResultSet rs = metaData.getColumns(null, null, tableName, columnName)) {
return !rs.next();
}
}
@Override
public void reload() {
close(con);
@ -146,7 +152,7 @@ public class SQLite implements DataSource {
PreparedStatement pst = null;
ResultSet rs = null;
try {
pst = con.prepareStatement("SELECT * FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);");
pst = con.prepareStatement("SELECT 1 FROM " + tableName + " WHERE LOWER(" + col.NAME + ")=LOWER(?);");
pst.setString(1, user);
rs = pst.executeQuery();
return rs.next();

View File

@ -18,20 +18,16 @@ public final class Settings {
public static List<String> getUnrestrictedName;
public static boolean isPermissionCheckEnabled;
public static boolean isTeleportToSpawnEnabled;
public static boolean isSessionsEnabled;
public static boolean isAllowRestrictedIp;
public static boolean isSaveQuitLocationEnabled;
public static boolean protectInventoryBeforeLogInEnabled;
public static boolean isStopEnabled;
public static boolean reloadSupport;
public static boolean noTeleport;
public static boolean isRemoveSpeedEnabled;
public static String getUnloggedinGroup;
public static String unRegisteredGroup;
public static String getRegisteredGroup;
public static String defaultWorld;
public static String crazyloginFileName;
public static int getSessionTimeout;
public static int getNonActivatedGroup;
private static FileConfiguration configFile;
@ -48,10 +44,7 @@ public final class Settings {
private static void loadVariables() {
isPermissionCheckEnabled = load(PluginSettings.ENABLE_PERMISSION_CHECK);
isTeleportToSpawnEnabled = load(RestrictionSettings.TELEPORT_UNAUTHED_TO_SPAWN);
isSessionsEnabled = load(PluginSettings.SESSIONS_ENABLED);
getSessionTimeout = configFile.getInt("settings.sessions.timeout", 10);
isAllowRestrictedIp = load(RestrictionSettings.ENABLE_RESTRICTED_USERS);
isRemoveSpeedEnabled = load(RestrictionSettings.REMOVE_SPEED);
isSaveQuitLocationEnabled = load(RestrictionSettings.SAVE_QUIT_LOCATION);
getUnloggedinGroup = load(SecuritySettings.UNLOGGEDIN_GROUP);
getNonActivatedGroup = configFile.getInt("ExternalBoardOptions.nonActivedUserGroup", -1);
@ -63,7 +56,6 @@ public final class Settings {
reloadSupport = configFile.getBoolean("Security.ReloadCommand.useReloadCommandSupport", true);
defaultWorld = configFile.getString("Purge.defaultWorld", "world");
noTeleport = load(RestrictionSettings.NO_TELEPORT);
crazyloginFileName = configFile.getString("Converter.CrazyLogin.fileName", "accounts.db");
}
/**

View File

@ -12,6 +12,9 @@ import java.util.Set;
import static fr.xephi.authme.AuthMeMatchers.equalToHash;
import static fr.xephi.authme.AuthMeMatchers.hasAuthBasicData;
import static fr.xephi.authme.AuthMeMatchers.hasAuthLocation;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
@ -170,6 +173,22 @@ public abstract class AbstractDataSourceIntegrationTest {
assertThat(dataSource.getPassword("user"), equalToHash("new_hash"));
}
@Test
public void shouldUpdatePasswordWithPlayerAuth() {
// given
DataSource dataSource = getDataSource("salt");
PlayerAuth bobbyAuth = PlayerAuth.builder().name("bobby").password(new HashedPassword("tt", "cc")).build();
PlayerAuth invalidAuth = PlayerAuth.builder().name("invalid").password(new HashedPassword("tt", "cc")).build();
// when
boolean response1 = dataSource.updatePassword(bobbyAuth);
boolean response2 = dataSource.updatePassword(invalidAuth);
// then
assertThat(response1 && response2, equalTo(true));
assertThat(dataSource.getPassword("bobby"), equalToHash("tt", "cc"));
}
@Test
public void shouldRemovePlayerAuth() {
// given
@ -321,4 +340,49 @@ public abstract class AbstractDataSourceIntegrationTest {
assertThat(dataSource.getAuth("bobby"), hasAuthBasicData("bobby", "BOBBY", "your@email.com", "123.45.67.89"));
}
@Test
public void shouldGetRecordsToPurge() {
// given
DataSource dataSource = getDataSource();
// 1453242857 -> user, 1449136800 -> bobby
// when
Set<String> records1 = dataSource.getRecordsToPurge(1450000000);
Set<String> records2 = dataSource.getRecordsToPurge(1460000000);
// then
assertThat(records1, contains("bobby"));
assertThat(records2, containsInAnyOrder("bobby", "user"));
// check that the entry was not deleted because of running this command
assertThat(dataSource.isAuthAvailable("bobby"), equalTo(true));
}
@Test
public void shouldPerformOperationsOnIsLoggedColumnSuccessfully() {
DataSource dataSource = getDataSource();
// on startup no one should be marked as logged
assertThat(dataSource.getLoggedPlayers(), empty());
// Mark user as logged
dataSource.setLogged("user");
// non-existent user should not break database
dataSource.setLogged("does-not-exist");
assertThat(dataSource.isLogged("user"), equalTo(true));
assertThat(dataSource.isLogged("bobby"), equalTo(false));
// Set bobby logged and unlog user
dataSource.setLogged("bobby");
dataSource.setUnlogged("user");
assertThat(dataSource.getLoggedPlayers(),
contains(hasAuthBasicData("bobby", "Bobby", "your@email.com", "123.45.67.89")));
// Set both as logged (even if Bobby already is logged)
dataSource.setLogged("user");
dataSource.setLogged("bobby");
dataSource.purgeLogged();
assertThat(dataSource.isLogged("user"), equalTo(false));
assertThat(dataSource.getLoggedPlayers(), empty());
}
}

View File

@ -6,6 +6,7 @@ import fr.xephi.authme.TestHelper;
import fr.xephi.authme.settings.NewSetting;
import fr.xephi.authme.settings.domain.Property;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.mockito.invocation.InvocationOnMock;
@ -60,7 +61,6 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest {
@Before
public void initializeConnectionAndTable() throws SQLException {
silentClose(hikariSource);
HikariConfig config = new HikariConfig();
config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource");
config.setConnectionTestQuery("VALUES 1");
@ -77,6 +77,11 @@ public class MySqlIntegrationTest extends AbstractDataSourceIntegrationTest {
hikariSource = ds;
}
@After
public void closeConnection() {
silentClose(hikariSource);
}
@Override
protected DataSource getDataSource(String saltColumn) {
when(settings.getProperty(DatabaseSettings.MYSQL_COL_SALT)).thenReturn(saltColumn);

View File

@ -1,11 +1,14 @@
package fr.xephi.authme.datasource;
import fr.xephi.authme.TestHelper;
import fr.xephi.authme.cache.auth.PlayerAuth;
import fr.xephi.authme.settings.NewSetting;
import fr.xephi.authme.settings.domain.Property;
import fr.xephi.authme.settings.properties.DatabaseSettings;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@ -17,6 +20,8 @@ import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -61,7 +66,6 @@ public class SQLiteIntegrationTest extends AbstractDataSourceIntegrationTest {
@Before
public void initializeConnectionAndTable() throws SQLException {
silentClose(con);
Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:");
try (Statement st = connection.createStatement()) {
st.execute("DROP TABLE IF EXISTS authme");
@ -72,6 +76,55 @@ public class SQLiteIntegrationTest extends AbstractDataSourceIntegrationTest {
con = connection;
}
@After
public void closeConnection() {
silentClose(con);
}
@Test
public void shouldSetUpTableIfMissing() throws SQLException {
// given
Statement st = con.createStatement();
// table is absent
st.execute("DROP TABLE authme");
SQLite sqLite = new SQLite(settings, con);
// when
sqLite.setup();
// then
// Save some player to verify database is operational
sqLite.saveAuth(PlayerAuth.builder().name("Name").build());
assertThat(sqLite.getAllAuths(), hasSize(1));
}
@Test
public void shouldCreateMissingColumns() throws SQLException {
// given
Statement st = con.createStatement();
// drop table and create one with only some of the columns: SQLite doesn't support ALTER TABLE t DROP COLUMN c
st.execute("DROP TABLE authme");
st.execute("CREATE TABLE authme ("
+ "id bigint, "
+ "username varchar(255) unique, "
+ "password varchar(255) not null, "
+ "primary key (id));");
SQLite sqLite = new SQLite(settings, con);
// when
sqLite.setup();
// then
// Save some player to verify database is operational
sqLite.saveAuth(PlayerAuth.builder().name("Name").build());
assertThat(sqLite.getAllAuths(), hasSize(1));
}
@Test
public void shouldCreate2() throws SQLException {
con.createStatement().execute("SELECT 1 from authme");
}
@Override
protected DataSource getDataSource(String saltColumn) {
when(settings.getProperty(DatabaseSettings.MYSQL_COL_SALT)).thenReturn(saltColumn);