diff --git a/pom.xml b/pom.xml index b05566938..fd994681a 100644 --- a/pom.xml +++ b/pom.xml @@ -376,6 +376,13 @@ compile true + + + org.xerial + sqlite-jdbc + 3.8.11.2 + test + diff --git a/src/main/java/fr/xephi/authme/cache/auth/PlayerAuth.java b/src/main/java/fr/xephi/authme/cache/auth/PlayerAuth.java index 41672c0c9..c2bb4f704 100644 --- a/src/main/java/fr/xephi/authme/cache/auth/PlayerAuth.java +++ b/src/main/java/fr/xephi/authme/cache/auth/PlayerAuth.java @@ -1,12 +1,11 @@ package fr.xephi.authme.cache.auth; +import fr.xephi.authme.security.crypts.HashedPassword; +import org.bukkit.Location; + import static com.google.common.base.Objects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; -import org.bukkit.Location; - -import fr.xephi.authme.security.crypts.HashedPassword; - /** */ @@ -85,20 +84,6 @@ public class PlayerAuth { this(nickname, new HashedPassword(hash), -1, ip, lastLogin, 0, 0, 0, "world", email, realName); } - /** - * Constructor for PlayerAuth. - * - * @param nickname String - * @param hash String - * @param salt String - * @param ip String - * @param lastLogin long - * @param realName String - */ - public PlayerAuth(String nickname, String hash, String salt, String ip, long lastLogin, String realName) { - this(nickname, new HashedPassword(hash, salt), -1, ip, lastLogin, 0, 0, 0, "world", "your@email.com", realName); - } - /** * Constructor for PlayerAuth. * @@ -118,44 +103,6 @@ public class PlayerAuth { this(nickname, new HashedPassword(hash), -1, ip, lastLogin, x, y, z, world, email, realName); } - /** - * Constructor for PlayerAuth. - * - * @param nickname String - * @param hash String - * @param salt String - * @param ip String - * @param lastLogin long - * @param x double - * @param y double - * @param z double - * @param world String - * @param email String - * @param realName String - */ - public PlayerAuth(String nickname, String hash, String salt, String ip, long lastLogin, double x, double y, - double z, String world, String email, String realName) { - this(nickname, new HashedPassword(hash, salt), -1, ip, lastLogin, - x, y, z, world, email, realName); - } - - /** - * Constructor for PlayerAuth. - * - * @param nickname String - * @param hash String - * @param salt String - * @param groupId int - * @param ip String - * @param lastLogin long - * @param realName String - */ - public PlayerAuth(String nickname, String hash, String salt, int groupId, String ip, - long lastLogin, String realName) { - this(nickname, new HashedPassword(hash, salt), groupId, ip, lastLogin, - 0, 0, 0, "world", "your@email.com", realName); - } - /** * Constructor for PlayerAuth. * @@ -171,8 +118,8 @@ public class PlayerAuth { * @param email String * @param realName String */ - public PlayerAuth(String nickname, HashedPassword password, int groupId, String ip, long lastLogin, - double x, double y, double z, String world, String email, String realName) { + private PlayerAuth(String nickname, HashedPassword password, int groupId, String ip, long lastLogin, + double x, double y, double z, String world, String email, String realName) { this.nickname = nickname.toLowerCase(); this.password = password; this.ip = ip; diff --git a/src/main/java/fr/xephi/authme/datasource/SQLite.java b/src/main/java/fr/xephi/authme/datasource/SQLite.java index 081e386cf..f27d8ef0b 100644 --- a/src/main/java/fr/xephi/authme/datasource/SQLite.java +++ b/src/main/java/fr/xephi/authme/datasource/SQLite.java @@ -1,5 +1,6 @@ package fr.xephi.authme.datasource; +import com.google.common.annotations.VisibleForTesting; import fr.xephi.authme.ConsoleLogger; import fr.xephi.authme.cache.auth.PlayerAuth; import fr.xephi.authme.security.crypts.HashedPassword; @@ -46,6 +47,21 @@ public class SQLite implements DataSource { } } + @VisibleForTesting + SQLite(NewSetting settings, Connection connection, boolean executeSetup) { + this.database = settings.getProperty(DatabaseSettings.MYSQL_DATABASE); + this.tableName = settings.getProperty(DatabaseSettings.MYSQL_TABLE); + this.col = new Columns(settings); + this.con = connection; + if (executeSetup) { + try { + setup(); + } catch (SQLException e) { + throw new IllegalStateException(e); + } + } + } + private synchronized void connect() throws ClassNotFoundException, SQLException { Class.forName("org.sqlite.JDBC"); ConsoleLogger.info("SQLite driver loaded"); diff --git a/src/test/java/fr/xephi/authme/TestHelper.java b/src/test/java/fr/xephi/authme/TestHelper.java index a7a60865f..06ff06a4b 100644 --- a/src/test/java/fr/xephi/authme/TestHelper.java +++ b/src/test/java/fr/xephi/authme/TestHelper.java @@ -2,6 +2,8 @@ package fr.xephi.authme; import java.io.File; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; /** * AuthMe test utilities. @@ -18,11 +20,31 @@ public final class TestHelper { * @return The project file */ public static File getJarFile(String path) { + URL url = getUrlOrThrow(path); + return new File(url.getFile()); + } + + /** + * Return a {@link Path} to a file in the JAR's resources (main or test). + * + * @param path The absolute path to the file + * @return The Path object to the file + */ + public static Path getJarPath(String path) { + String sqlFilePath = getUrlOrThrow(path).getPath(); + // Windows preprends the path with a '/' or '\', which Paths cannot handle + String appropriatePath = System.getProperty("os.name").contains("indow") + ? sqlFilePath.substring(1) + : sqlFilePath; + return Paths.get(appropriatePath); + } + + private static URL getUrlOrThrow(String path) { URL url = TestHelper.class.getResource(path); if (url == null) { throw new IllegalStateException("File '" + path + "' could not be loaded"); } - return new File(url.getFile()); + return url; } } diff --git a/src/test/java/fr/xephi/authme/datasource/AuthMeMatchers.java b/src/test/java/fr/xephi/authme/datasource/AuthMeMatchers.java new file mode 100644 index 000000000..3bb8af816 --- /dev/null +++ b/src/test/java/fr/xephi/authme/datasource/AuthMeMatchers.java @@ -0,0 +1,91 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.cache.auth.PlayerAuth; +import fr.xephi.authme.security.crypts.HashedPassword; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.util.Objects; + +/** + * Custom matchers for AuthMe entities. + */ +public final class AuthMeMatchers { + + private AuthMeMatchers() { + } + + public static Matcher equalToHash(final String hash) { + return equalToHash(hash, null); + } + + public static Matcher equalToHash(final String hash, final String salt) { + return new BaseMatcher() { + @Override + public boolean matches(Object item) { + if (item instanceof HashedPassword) { + HashedPassword input = (HashedPassword) item; + return Objects.equals(hash, input.getHash()) && Objects.equals(salt, input.getSalt()); + } + return false; + } + + @Override + public void describeTo(Description description) { + String representation = "'" + hash + "'"; + if (salt != null) { + representation += ", '" + salt + "'"; + } + description.appendValue("HashedPassword(" + representation + ")"); + } + }; + } + + public static Matcher hasAuthBasicData(final String name, final String realName, + final String email, final String ip) { + return new BaseMatcher() { + @Override + public boolean matches(Object item) { + if (item instanceof PlayerAuth) { + PlayerAuth input = (PlayerAuth) item; + return Objects.equals(name, input.getNickname()) + && Objects.equals(realName, input.getRealName()) + && Objects.equals(email, input.getEmail()) + && Objects.equals(ip, input.getIp()); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendValue(String.format("PlayerAuth with name %s, realname %s, email %s, ip %s", + name, realName, email, ip)); + } + }; + } + + public static Matcher hasAuthLocation(final double x, final double y, final double z, + final String world) { + return new BaseMatcher() { + @Override + public boolean matches(Object item) { + if (item instanceof PlayerAuth) { + PlayerAuth input = (PlayerAuth) item; + return Objects.equals(x, input.getQuitLocX()) + && Objects.equals(y, input.getQuitLocY()) + && Objects.equals(z, input.getQuitLocZ()) + && Objects.equals(world, input.getWorld()); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendValue(String.format("PlayerAuth with quit location (x: %f, y: %f, z: %f, world: %s)", + x, y, z, world)); + } + }; + } + +} diff --git a/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java b/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java new file mode 100644 index 000000000..e23126356 --- /dev/null +++ b/src/test/java/fr/xephi/authme/datasource/SQLiteIntegrationTest.java @@ -0,0 +1,160 @@ +package fr.xephi.authme.datasource; + +import fr.xephi.authme.ConsoleLoggerTestInitializer; +import fr.xephi.authme.TestHelper; +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.domain.Property; +import fr.xephi.authme.settings.properties.DatabaseSettings; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import static fr.xephi.authme.datasource.AuthMeMatchers.equalToHash; +import static fr.xephi.authme.datasource.AuthMeMatchers.hasAuthBasicData; +import static fr.xephi.authme.datasource.AuthMeMatchers.hasAuthLocation; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Integration test for {@link SQLite}. + */ +public class SQLiteIntegrationTest { + + /** Mock for a settings instance. */ + private static NewSetting settings; + /** Collection of SQL statements to execute for initialization of a test. */ + private static String[] sqlInitialize; + /** Connection to the SQLite test database. */ + private Connection con; + + /** + * Set up the settings mock to return specific values for database settings and load {@link #sqlInitialize}. + */ + @BeforeClass + public static void initializeSettings() throws IOException, ClassNotFoundException { + // Check that we have an implementation for SQLite + Class.forName("org.sqlite.JDBC"); + + settings = mock(NewSetting.class); + when(settings.getProperty(any(Property.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return ((Property) invocation.getArguments()[0]).getDefaultValue(); + } + }); + set(DatabaseSettings.MYSQL_DATABASE, "sqlite-test"); + set(DatabaseSettings.MYSQL_TABLE, "authme"); + set(DatabaseSettings.MYSQL_COL_SALT, "salt"); + ConsoleLoggerTestInitializer.setupLogger(); + + Path sqlInitFile = TestHelper.getJarPath("/datasource-integration/sqlite-initialize.sql"); + // Note ljacqu 20160221: It appears that we can only run one statement per Statement.execute() so we split + // the SQL file by ";\n" as to get the individual statements + sqlInitialize = new String(Files.readAllBytes(sqlInitFile)).split(";\\n"); + } + + @Before + public void initializeConnectionAndTable() throws SQLException, ClassNotFoundException { + silentClose(con); + Connection connection = DriverManager.getConnection("jdbc:sqlite::memory:"); + try (Statement st = connection.createStatement()) { + st.execute("DROP TABLE IF EXISTS authme"); + for (String statement : sqlInitialize) { + st.execute(statement); + } + } + con = connection; + } + + @Test + public void shouldReturnIfAuthIsAvailableOrNot() { + // given + DataSource dataSource = new SQLite(settings, con, false); + + // when + boolean bobby = dataSource.isAuthAvailable("bobby"); + boolean chris = dataSource.isAuthAvailable("chris"); + boolean user = dataSource.isAuthAvailable("USER"); + + // then + assertThat(bobby, equalTo(true)); + assertThat(chris, equalTo(false)); + assertThat(user, equalTo(true)); + } + + @Test + public void shouldReturnPassword() { + // given + DataSource dataSource = new SQLite(settings, con, false); + + // when + HashedPassword bobbyPassword = dataSource.getPassword("bobby"); + HashedPassword invalidPassword = dataSource.getPassword("doesNotExist"); + HashedPassword userPassword = dataSource.getPassword("user"); + + // then + assertThat(bobbyPassword, equalToHash( + "$SHA$11aa0706173d7272$dbba96681c2ae4e0bfdf226d70fbbc5e4ee3d8071faa613bc533fe8a64817d10")); + assertThat(invalidPassword, nullValue()); + assertThat(userPassword, equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); + } + + @Test + public void shouldGetAuth() { + // given + DataSource dataSource = new SQLite(settings, con, false); + + // when + PlayerAuth invalidAuth = dataSource.getAuth("notInDB"); + PlayerAuth bobbyAuth = dataSource.getAuth("Bobby"); + PlayerAuth userAuth = dataSource.getAuth("user"); + + // then + assertThat(invalidAuth, nullValue()); + + assertThat(bobbyAuth, hasAuthBasicData("bobby", "Bobby", "your@email.com", "123.45.67.89")); + assertThat(bobbyAuth, hasAuthLocation(1.05, 2.1, 4.2, "world")); + assertThat(bobbyAuth.getLastLogin(), equalTo(1449136800L)); + assertThat(bobbyAuth.getPassword(), equalToHash( + "$SHA$11aa0706173d7272$dbba96681c2ae4e0bfdf226d70fbbc5e4ee3d8071faa613bc533fe8a64817d10")); + + assertThat(userAuth, hasAuthBasicData("user", "user", "user@example.org", "34.56.78.90")); + assertThat(userAuth, hasAuthLocation(124.1, 76.3, -127.8, "nether")); + assertThat(userAuth.getLastLogin(), equalTo(1453242857L)); + assertThat(userAuth.getPassword(), equalToHash("b28c32f624a4eb161d6adc9acb5bfc5b", "f750ba32")); + } + + private static void set(Property property, T value) { + when(settings.getProperty(property)).thenReturn(value); + } + + private static void silentClose(Connection con) { + if (con != null) { + try { + if (!con.isClosed()) { + con.close(); + } + } catch (SQLException e) { + // silent + } + } + } + + +} diff --git a/src/test/resources/datasource-integration/sqlite-initialize.sql b/src/test/resources/datasource-integration/sqlite-initialize.sql new file mode 100644 index 000000000..dc06fcaba --- /dev/null +++ b/src/test/resources/datasource-integration/sqlite-initialize.sql @@ -0,0 +1,22 @@ +-- Important: separate SQL statements by ; followed directly by a newline. We split the file contents by ";\n" + +CREATE TABLE authme ( + id INTEGER AUTO_INCREMENT, + username VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + ip VARCHAR(40) NOT NULL, + lastlogin BIGINT, + x DOUBLE NOT NULL DEFAULT '0.0', + y DOUBLE NOT NULL DEFAULT '0.0', + z DOUBLE NOT NULL DEFAULT '0.0', + world VARCHAR(255) NOT NULL DEFAULT 'world', + email VARCHAR(255) DEFAULT 'your@email.com', + isLogged INT DEFAULT '0', realname VARCHAR(255) NOT NULL DEFAULT 'Player', + salt varchar(255), + CONSTRAINT table_const_prim PRIMARY KEY (id) +); + +INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, email, isLogged, realname, salt) +VALUES (1,'bobby','$SHA$11aa0706173d7272$dbba96681c2ae4e0bfdf226d70fbbc5e4ee3d8071faa613bc533fe8a64817d10','123.45.67.89',1449136800,1.05,2.1,4.2,'world','your@email.com',0,'Bobby',NULL); +INSERT INTO authme (id, username, password, ip, lastlogin, x, y, z, world, email, isLogged, realname, salt) +VALUES (NULL,'user','b28c32f624a4eb161d6adc9acb5bfc5b','34.56.78.90',1453242857,124.1,76.3,-127.8,'nether','user@example.org',0,'user','f750ba32');