diff --git a/bramble-core/build.gradle b/bramble-core/build.gradle index 59361dc93eedde403d9044802caedaf426094e55..9307f5d583a86d0b921954bb40bfd512d5edc285 100644 --- a/bramble-core/build.gradle +++ b/bramble-core/build.gradle @@ -9,13 +9,14 @@ apply plugin: 'witness' dependencies { implementation project(path: ':bramble-api', configuration: 'default') implementation 'com.madgag.spongycastle:core:1.58.0.0' - implementation 'com.h2database:h2:1.4.192' // This is the last version that supports Java 1.6 + implementation 'com.h2database:h2:1.4.192' // The last version that supports Java 1.6 implementation 'org.bitlet:weupnp:0.1.4' implementation 'net.i2p.crypto:eddsa:0.2.0' apt 'com.google.dagger:dagger-compiler:2.0.2' testImplementation project(path: ':bramble-api', configuration: 'testOutput') + testImplementation 'org.hsqldb:hsqldb:2.3.5' // The last version that supports Java 1.6 testImplementation 'junit:junit:4.12' testImplementation "org.jmock:jmock:2.8.2" testImplementation "org.jmock:jmock-junit4:2.8.2" diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java index 32596eede3aa70fae92e160b0d4729d5b3029700..307f146e19dc6a819c4e53a760d1933c9e5df60a 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/H2Database.java @@ -22,16 +22,18 @@ import javax.inject.Inject; class H2Database extends JdbcDatabase { private static final String HASH_TYPE = "BINARY(32)"; + private static final String SECRET_TYPE = "BINARY(32)"; private static final String BINARY_TYPE = "BINARY"; private static final String COUNTER_TYPE = "INT NOT NULL AUTO_INCREMENT"; - private static final String SECRET_TYPE = "BINARY(32)"; + private static final String STRING_TYPE = "VARCHAR"; private final DatabaseConfig config; private final String url; @Inject H2Database(DatabaseConfig config, Clock clock) { - super(HASH_TYPE, BINARY_TYPE, COUNTER_TYPE, SECRET_TYPE, clock); + super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE, + clock); this.config = config; File dir = config.getDatabaseDirectory(); String path = new File(dir, "db").getAbsolutePath(); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java new file mode 100644 index 0000000000000000000000000000000000000000..1db9248cb28b6d67f7de3818b822480296dabf5e --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/HyperSqlDatabase.java @@ -0,0 +1,99 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.db.DatabaseConfig; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.util.StringUtils; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.inject.Inject; + +/** + * Contains all the HSQLDB-specific code for the database. + */ +@NotNullByDefault +class HyperSqlDatabase extends JdbcDatabase { + + private static final String HASH_TYPE = "BINARY(32)"; + private static final String SECRET_TYPE = "BINARY(32)"; + private static final String BINARY_TYPE = "BINARY"; + private static final String COUNTER_TYPE = + "INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY(START WITH 1)"; + private static final String STRING_TYPE = "VARCHAR"; + + private final DatabaseConfig config; + private final String url; + + @Inject + HyperSqlDatabase(DatabaseConfig config, Clock clock) { + super(HASH_TYPE, SECRET_TYPE, BINARY_TYPE, COUNTER_TYPE, STRING_TYPE, + clock); + this.config = config; + File dir = config.getDatabaseDirectory(); + String path = new File(dir, "db").getAbsolutePath(); + url = "jdbc:hsqldb:file:" + path + + ";sql.enforce_size=false;allow_empty_batch=true" + + ";encrypt_lobs=true;crypt_type=AES"; + } + + @Override + public boolean open() throws DbException { + boolean reopen = config.databaseExists(); + if (!reopen) config.getDatabaseDirectory().mkdirs(); + super.open("org.hsqldb.jdbc.JDBCDriver", reopen); + return reopen; + } + + @Override + public void close() throws DbException { + try { + super.closeAllConnections(); + Connection c = createConnection(); + Statement s = c.createStatement(); + s.executeQuery("SHUTDOWN"); + s.close(); + c.close(); + } catch (SQLException e) { + throw new DbException(e); + } + } + + @Override + public long getFreeSpace() throws DbException { + File dir = config.getDatabaseDirectory(); + long maxSize = config.getMaxSize(); + long free = dir.getFreeSpace(); + long used = getDiskSpace(dir); + long quota = maxSize - used; + return Math.min(free, quota); + } + + private long getDiskSpace(File f) { + if (f.isDirectory()) { + long total = 0; + File[] children = f.listFiles(); + if (children != null) + for (File child : children) total += getDiskSpace(child); + return total; + } else if (f.isFile()) { + return f.length(); + } else { + return 0; + } + } + + @Override + protected Connection createConnection() throws SQLException { + SecretKey key = config.getEncryptionKey(); + if (key == null) throw new IllegalStateException(); + String hex = StringUtils.toHexString(key.getBytes()); + return DriverManager.getConnection(url + ";crypt_key=" + hex); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java index 8b8c3562af85c0b92dce12b71056bc93198f0c82..bf863ade1d8692bf3794489f89f628bf2fa6f7f2 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java @@ -73,27 +73,27 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_SETTINGS = "CREATE TABLE settings" - + " (namespace VARCHAR NOT NULL," - + " key VARCHAR NOT NULL," - + " value VARCHAR NOT NULL," - + " PRIMARY KEY (namespace, key))"; + + " (namespace _STRING NOT NULL," + + " \"key\" _STRING NOT NULL," + + " value _STRING NOT NULL," + + " PRIMARY KEY (namespace, \"key\"))"; private static final String CREATE_LOCAL_AUTHORS = "CREATE TABLE localAuthors" - + " (authorId HASH NOT NULL," - + " name VARCHAR NOT NULL," - + " publicKey BINARY NOT NULL," - + " privateKey BINARY NOT NULL," + + " (authorId _HASH NOT NULL," + + " name _STRING NOT NULL," + + " publicKey _BINARY NOT NULL," + + " privateKey _BINARY NOT NULL," + " created BIGINT NOT NULL," + " PRIMARY KEY (authorId))"; private static final String CREATE_CONTACTS = "CREATE TABLE contacts" - + " (contactId COUNTER," - + " authorId HASH NOT NULL," - + " name VARCHAR NOT NULL," - + " publicKey BINARY NOT NULL," - + " localAuthorId HASH NOT NULL," + + " (contactId _COUNTER," + + " authorId _HASH NOT NULL," + + " name _STRING NOT NULL," + + " publicKey _BINARY NOT NULL," + + " localAuthorId _HASH NOT NULL," + " verified BOOLEAN NOT NULL," + " active BOOLEAN NOT NULL," + " PRIMARY KEY (contactId)," @@ -103,17 +103,17 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_GROUPS = "CREATE TABLE groups" - + " (groupId HASH NOT NULL," - + " clientId VARCHAR NOT NULL," - + " descriptor BINARY NOT NULL," + + " (groupId _HASH NOT NULL," + + " clientId _STRING NOT NULL," + + " descriptor _BINARY NOT NULL," + " PRIMARY KEY (groupId))"; private static final String CREATE_GROUP_METADATA = "CREATE TABLE groupMetadata" - + " (groupId HASH NOT NULL," - + " key VARCHAR NOT NULL," - + " value BINARY NOT NULL," - + " PRIMARY KEY (groupId, key)," + + " (groupId _HASH NOT NULL," + + " \"key\" _STRING NOT NULL," + + " value _BINARY NOT NULL," + + " PRIMARY KEY (groupId, \"key\")," + " FOREIGN KEY (groupId)" + " REFERENCES groups (groupId)" + " ON DELETE CASCADE)"; @@ -121,7 +121,7 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_GROUP_VISIBILITIES = "CREATE TABLE groupVisibilities" + " (contactId INT NOT NULL," - + " groupId HASH NOT NULL," + + " groupId _HASH NOT NULL," + " shared BOOLEAN NOT NULL," + " PRIMARY KEY (contactId, groupId)," + " FOREIGN KEY (contactId)" @@ -133,8 +133,8 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_MESSAGES = "CREATE TABLE messages" - + " (messageId HASH NOT NULL," - + " groupId HASH NOT NULL," + + " (messageId _HASH NOT NULL," + + " groupId _HASH NOT NULL," + " timestamp BIGINT NOT NULL," + " state INT NOT NULL," + " shared BOOLEAN NOT NULL," @@ -147,19 +147,19 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_MESSAGE_METADATA = "CREATE TABLE messageMetadata" - + " (messageId HASH NOT NULL," - + " key VARCHAR NOT NULL," - + " value BINARY NOT NULL," - + " PRIMARY KEY (messageId, key)," + + " (messageId _HASH NOT NULL," + + " \"key\" _STRING NOT NULL," + + " value _BINARY NOT NULL," + + " PRIMARY KEY (messageId, \"key\")," + " FOREIGN KEY (messageId)" + " REFERENCES messages (messageId)" + " ON DELETE CASCADE)"; private static final String CREATE_MESSAGE_DEPENDENCIES = "CREATE TABLE messageDependencies" - + " (groupId HASH NOT NULL," - + " messageId HASH NOT NULL," - + " dependencyId HASH NOT NULL," // Not a foreign key + + " (groupId _HASH NOT NULL," + + " messageId _HASH NOT NULL," + + " dependencyId _HASH NOT NULL," // Not a foreign key + " FOREIGN KEY (groupId)" + " REFERENCES groups (groupId)" + " ON DELETE CASCADE," @@ -169,7 +169,7 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_OFFERS = "CREATE TABLE offers" - + " (messageId HASH NOT NULL," // Not a foreign key + + " (messageId _HASH NOT NULL," // Not a foreign key + " contactId INT NOT NULL," + " PRIMARY KEY (messageId, contactId)," + " FOREIGN KEY (contactId)" @@ -178,7 +178,7 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_STATUSES = "CREATE TABLE statuses" - + " (messageId HASH NOT NULL," + + " (messageId _HASH NOT NULL," + " contactId INT NOT NULL," + " ack BOOLEAN NOT NULL," + " seen BOOLEAN NOT NULL," @@ -195,20 +195,20 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_TRANSPORTS = "CREATE TABLE transports" - + " (transportId VARCHAR NOT NULL," + + " (transportId _STRING NOT NULL," + " maxLatency INT NOT NULL," + " PRIMARY KEY (transportId))"; private static final String CREATE_INCOMING_KEYS = "CREATE TABLE incomingKeys" + " (contactId INT NOT NULL," - + " transportId VARCHAR NOT NULL," - + " period BIGINT NOT NULL," - + " tagKey SECRET NOT NULL," - + " headerKey SECRET NOT NULL," + + " transportId _STRING NOT NULL," + + " \"period\" BIGINT NOT NULL," + + " tagKey _SECRET NOT NULL," + + " headerKey _SECRET NOT NULL," + " base BIGINT NOT NULL," - + " bitmap BINARY NOT NULL," - + " PRIMARY KEY (contactId, transportId, period)," + + " bitmap _BINARY NOT NULL," + + " PRIMARY KEY (contactId, transportId, \"period\")," + " FOREIGN KEY (contactId)" + " REFERENCES contacts (contactId)" + " ON DELETE CASCADE," @@ -219,10 +219,10 @@ abstract class JdbcDatabase implements Database<Connection> { private static final String CREATE_OUTGOING_KEYS = "CREATE TABLE outgoingKeys" + " (contactId INT NOT NULL," - + " transportId VARCHAR NOT NULL," - + " period BIGINT NOT NULL," - + " tagKey SECRET NOT NULL," - + " headerKey SECRET NOT NULL," + + " transportId _STRING NOT NULL," + + " \"period\" BIGINT NOT NULL," + + " tagKey _SECRET NOT NULL," + + " headerKey _SECRET NOT NULL," + " stream BIGINT NOT NULL," + " PRIMARY KEY (contactId, transportId)," + " FOREIGN KEY (contactId)" @@ -260,7 +260,8 @@ abstract class JdbcDatabase implements Database<Connection> { Logger.getLogger(JdbcDatabase.class.getName()); // Different database libraries use different names for certain types - private final String hashType, binaryType, counterType, secretType; + private final String hashType, secretType, binaryType; + private final String counterType, stringType; private final Clock clock; // Locking: connectionsLock @@ -275,12 +276,13 @@ abstract class JdbcDatabase implements Database<Connection> { private final Lock connectionsLock = new ReentrantLock(); private final Condition connectionsChanged = connectionsLock.newCondition(); - JdbcDatabase(String hashType, String binaryType, String counterType, - String secretType, Clock clock) { + JdbcDatabase(String hashType, String secretType, String binaryType, + String counterType, String stringType, Clock clock) { this.hashType = hashType; + this.secretType = secretType; this.binaryType = binaryType; this.counterType = counterType; - this.secretType = secretType; + this.stringType = stringType; this.clock = clock; } @@ -383,10 +385,11 @@ abstract class JdbcDatabase implements Database<Connection> { } private String insertTypeNames(String s) { - s = s.replaceAll("HASH", hashType); - s = s.replaceAll("BINARY", binaryType); - s = s.replaceAll("COUNTER", counterType); - s = s.replaceAll("SECRET", secretType); + s = s.replaceAll("_HASH", hashType); + s = s.replaceAll("_SECRET", secretType); + s = s.replaceAll("_BINARY", binaryType); + s = s.replaceAll("_COUNTER", counterType); + s = s.replaceAll("_STRING", stringType); return s; } @@ -500,7 +503,8 @@ abstract class JdbcDatabase implements Database<Connection> { try { // Create a contact row String sql = "INSERT INTO contacts" - + " (authorId, name, publicKey, localAuthorId, verified, active)" + + " (authorId, name, publicKey, localAuthorId," + + " verified, active)" + " VALUES (?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, remote.getId().getBytes()); @@ -719,7 +723,7 @@ abstract class JdbcDatabase implements Database<Connection> { try { // Store the incoming keys String sql = "INSERT INTO incomingKeys (contactId, transportId," - + " period, tagKey, headerKey, base, bitmap)" + + " \"period\", tagKey, headerKey, base, bitmap)" + " VALUES (?, ?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); @@ -754,8 +758,8 @@ abstract class JdbcDatabase implements Database<Connection> { if (rows != 1) throw new DbStateException(); ps.close(); // Store the outgoing keys - sql = "INSERT INTO outgoingKeys (contactId, transportId, period," - + " tagKey, headerKey, stream)" + sql = "INSERT INTO outgoingKeys (contactId, transportId," + + " \"period\", tagKey, headerKey, stream)" + " VALUES (?, ?, ?, ?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); @@ -1335,7 +1339,7 @@ abstract class JdbcDatabase implements Database<Connection> { + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" + " WHERE state = ? AND groupId = ?" - + " AND key = ? AND value = ?"; + + " AND \"key\" = ? AND value = ?"; for (Entry<String, byte[]> e : query.entrySet()) { ps = txn.prepareStatement(sql); ps.setInt(1, DELIVERED.getValue()); @@ -1367,7 +1371,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT m.messageId, key, value" + String sql = "SELECT m.messageId, \"key\", value" + " FROM messages AS m" + " JOIN messageMetadata AS md" + " ON m.messageId = md.messageId" @@ -1417,7 +1421,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM groupMetadata" + String sql = "SELECT \"key\", value FROM groupMetadata" + " WHERE groupId = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, g.getBytes()); @@ -1440,7 +1444,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM messageMetadata AS md" + String sql = "SELECT \"key\", value FROM messageMetadata AS md" + " JOIN messages AS m" + " ON m.messageId = md.messageId" + " WHERE m.state = ? AND md.messageId = ?"; @@ -1466,7 +1470,7 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM messageMetadata AS md" + String sql = "SELECT \"key\", value FROM messageMetadata AS md" + " JOIN messages AS m" + " ON m.messageId = md.messageId" + " WHERE (m.state = ? OR m.state = ?)" @@ -1904,7 +1908,8 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; ResultSet rs = null; try { - String sql = "SELECT key, value FROM settings WHERE namespace = ?"; + String sql = "SELECT \"key\", value FROM settings" + + " WHERE namespace = ?"; ps = txn.prepareStatement(sql); ps.setString(1, namespace); rs = ps.executeQuery(); @@ -1927,10 +1932,10 @@ abstract class JdbcDatabase implements Database<Connection> { ResultSet rs = null; try { // Retrieve the incoming keys - String sql = "SELECT period, tagKey, headerKey, base, bitmap" + String sql = "SELECT \"period\", tagKey, headerKey, base, bitmap" + " FROM incomingKeys" + " WHERE transportId = ?" - + " ORDER BY contactId, period"; + + " ORDER BY contactId, \"period\""; ps = txn.prepareStatement(sql); ps.setString(1, t.getString()); rs = ps.executeQuery(); @@ -1947,10 +1952,10 @@ abstract class JdbcDatabase implements Database<Connection> { rs.close(); ps.close(); // Retrieve the outgoing keys in the same order - sql = "SELECT contactId, period, tagKey, headerKey, stream" + sql = "SELECT contactId, \"period\", tagKey, headerKey, stream" + " FROM outgoingKeys" + " WHERE transportId = ?" - + " ORDER BY contactId, period"; + + " ORDER BY contactId, \"period\""; ps = txn.prepareStatement(sql); ps.setString(1, t.getString()); rs = ps.executeQuery(); @@ -1987,7 +1992,8 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; try { String sql = "UPDATE outgoingKeys SET stream = stream + 1" - + " WHERE contactId = ? AND transportId = ? AND period = ?"; + + " WHERE contactId = ? AND transportId = ?" + + " AND \"period\" = ?"; ps = txn.prepareStatement(sql); ps.setInt(1, c.getInt()); ps.setString(2, t.getString()); @@ -2081,7 +2087,7 @@ abstract class JdbcDatabase implements Database<Connection> { // Delete any keys that are being removed if (!removed.isEmpty()) { String sql = "DELETE FROM " + tableName - + " WHERE " + columnName + " = ? AND key = ?"; + + " WHERE " + columnName + " = ? AND \"key\" = ?"; ps = txn.prepareStatement(sql); ps.setBytes(1, id); for (String key : removed) { @@ -2100,7 +2106,7 @@ abstract class JdbcDatabase implements Database<Connection> { if (retained.isEmpty()) return; // Update any keys that already exist String sql = "UPDATE " + tableName + " SET value = ?" - + " WHERE " + columnName + " = ? AND key = ?"; + + " WHERE " + columnName + " = ? AND \"key\" = ?"; ps = txn.prepareStatement(sql); ps.setBytes(2, id); for (Entry<String, byte[]> e : retained.entrySet()) { @@ -2117,7 +2123,7 @@ abstract class JdbcDatabase implements Database<Connection> { } // Insert any keys that don't already exist sql = "INSERT INTO " + tableName - + " (" + columnName + ", key, value)" + + " (" + columnName + ", \"key\", value)" + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); ps.setBytes(1, id); @@ -2149,7 +2155,7 @@ abstract class JdbcDatabase implements Database<Connection> { try { // Update any settings that already exist String sql = "UPDATE settings SET value = ?" - + " WHERE namespace = ? AND key = ?"; + + " WHERE namespace = ? AND \"key\" = ?"; ps = txn.prepareStatement(sql); for (Entry<String, String> e : s.entrySet()) { ps.setString(1, e.getValue()); @@ -2164,7 +2170,7 @@ abstract class JdbcDatabase implements Database<Connection> { if (rows > 1) throw new DbStateException(); } // Insert any settings that don't already exist - sql = "INSERT INTO settings (namespace, key, value)" + sql = "INSERT INTO settings (namespace, \"key\", value)" + " VALUES (?, ?, ?)"; ps = txn.prepareStatement(sql); int updateIndex = 0, inserted = 0; @@ -2528,7 +2534,8 @@ abstract class JdbcDatabase implements Database<Connection> { PreparedStatement ps = null; try { String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?" - + " WHERE contactId = ? AND transportId = ? AND period = ?"; + + " WHERE contactId = ? AND transportId = ?" + + " AND \"period\" = ?"; ps = txn.prepareStatement(sql); ps.setLong(1, base); ps.setBytes(2, bitmap); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..dbdeca4dcc943745504bfece15c85313c616c385 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicDatabaseTest.java @@ -0,0 +1,380 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.test.TestUtils; +import org.briarproject.bramble.util.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import static java.sql.Types.BINARY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public abstract class BasicDatabaseTest extends BrambleTestCase { + + private static final int BATCH_SIZE = 100; + + private final File testDir = TestUtils.getTestDirectory(); + private final File db = new File(testDir, "db"); + + protected abstract String getBinaryType(); + + protected abstract String getDriverName(); + + protected abstract Connection openConnection(File db, boolean encrypt) + throws SQLException; + + protected abstract void shutdownDatabase(File db, boolean encrypt) + throws SQLException; + + @Before + public void setUp() throws Exception { + testDir.mkdirs(); + Class.forName(getDriverName()); + } + + @Test + public void testInsertUpdateAndDelete() throws Exception { + Connection connection = openConnection(db, false); + try { + // Create the table + createTable(connection); + // Generate an ID and two names + byte[] id = TestUtils.getRandomId(); + String oldName = StringUtils.getRandomString(50); + String newName = StringUtils.getRandomString(50); + // Insert the ID and old name into the table + insertRow(connection, id, oldName); + // Check that the old name can be retrieved using the ID + assertTrue(rowExists(connection, id)); + assertEquals(oldName, getName(connection, id)); + // Update the name + updateRow(connection, id, newName); + // Check that the new name can be retrieved using the ID + assertTrue(rowExists(connection, id)); + assertEquals(newName, getName(connection, id)); + // Delete the row from the table + assertTrue(deleteRow(connection, id)); + // Check that the row no longer exists + assertFalse(rowExists(connection, id)); + // Deleting the row again should have no effect + assertFalse(deleteRow(connection, id)); + } finally { + connection.close(); + shutdownDatabase(db, false); + } + } + + @Test + public void testBatchInsertUpdateAndDelete() throws Exception { + Connection connection = openConnection(db, false); + try { + // Create the table + createTable(connection); + // Generate some IDs and two sets of names + byte[][] ids = new byte[BATCH_SIZE][]; + String[] oldNames = new String[BATCH_SIZE]; + String[] newNames = new String[BATCH_SIZE]; + for (int i = 0; i < BATCH_SIZE; i++) { + ids[i] = TestUtils.getRandomId(); + oldNames[i] = StringUtils.getRandomString(50); + newNames[i] = StringUtils.getRandomString(50); + } + // Insert the IDs and old names into the table as a batch + insertBatch(connection, ids, oldNames); + // Update the names as a batch + updateBatch(connection, ids, newNames); + // Check that the new names can be retrieved using the IDs + for (int i = 0; i < BATCH_SIZE; i++) { + assertTrue(rowExists(connection, ids[i])); + assertEquals(newNames[i], getName(connection, ids[i])); + } + // Delete the rows as a batch + boolean[] deleted = deleteBatch(connection, ids); + // Check that the rows no longer exist + for (int i = 0; i < BATCH_SIZE; i++) { + assertTrue(deleted[i]); + assertFalse(rowExists(connection, ids[i])); + } + // Deleting the rows again should have no effect + deleted = deleteBatch(connection, ids); + for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]); + } finally { + connection.close(); + shutdownDatabase(db, false); + } + } + + @Test + public void testSortOrder() throws Exception { + byte[] first = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + }; + byte[] second = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 127 + }; + byte[] third = new byte[] { + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, (byte) 255 + }; + Connection connection = openConnection(db, false); + try { + // Create the table + createTable(connection); + // Insert the rows + insertRow(connection, first, "first"); + insertRow(connection, second, "second"); + insertRow(connection, third, "third"); + insertRow(connection, null, "null"); + // Check the ordering of the < operator: null is not comparable + assertNull(getPredecessor(connection, first)); + assertEquals("first", getPredecessor(connection, second)); + assertEquals("second", getPredecessor(connection, third)); + assertNull(getPredecessor(connection, null)); + // Check the ordering of ORDER BY: nulls come first + List<String> names = getNames(connection); + assertEquals(4, names.size()); + assertEquals("null", names.get(0)); + assertEquals("first", names.get(1)); + assertEquals("second", names.get(2)); + assertEquals("third", names.get(3)); + } finally { + connection.close(); + shutdownDatabase(db, false); + } + } + + @Test + public void testDataIsFoundWithoutEncryption() throws Exception { + testEncryption(false); + } + + @Test + public void testDataIsNotFoundWithEncryption() throws Exception { + testEncryption(true); + } + + private void testEncryption(boolean encrypt) throws Exception { + byte[] sequence = new byte[] {'a', 'b', 'c', 'd', 'e', 'f', 'g'}; + Connection connection = openConnection(db, encrypt); + try { + createTable(connection); + insertRow(connection, sequence, "abcdefg"); + } finally { + connection.close(); + shutdownDatabase(db, encrypt); + } + try { + if (findSequence(testDir, sequence) == encrypt) fail(); + } finally { + shutdownDatabase(db, encrypt); + } + } + + private void createTable(Connection connection) throws SQLException { + Statement s = connection.createStatement(); + String sql = "CREATE TABLE foo (uniqueId " + getBinaryType() + "," + + " name VARCHAR(100) NOT NULL)"; + s.executeUpdate(sql); + s.close(); + } + + private void insertRow(Connection connection, byte[] id, String name) + throws SQLException { + String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)"; + PreparedStatement ps = connection.prepareStatement(sql); + if (id == null) ps.setNull(1, BINARY); + else ps.setBytes(1, id); + ps.setString(2, name); + int affected = ps.executeUpdate(); + assertEquals(1, affected); + ps.close(); + } + + private boolean rowExists(Connection connection, byte[] id) + throws SQLException { + assertNotNull(id); + String sql = "SELECT * FROM foo WHERE uniqueID = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + ps.setBytes(1, id); + ResultSet rs = ps.executeQuery(); + boolean found = rs.next(); + assertFalse(rs.next()); + rs.close(); + ps.close(); + return found; + } + + private String getName(Connection connection, byte[] id) + throws SQLException { + assertNotNull(id); + String sql = "SELECT name FROM foo WHERE uniqueID = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + ps.setBytes(1, id); + ResultSet rs = ps.executeQuery(); + assertTrue(rs.next()); + String name = rs.getString(1); + assertFalse(rs.next()); + rs.close(); + ps.close(); + return name; + } + + private void updateRow(Connection connection, byte[] id, String name) + throws SQLException { + String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + if (id == null) ps.setNull(2, BINARY); + else ps.setBytes(2, id); + ps.setString(1, name); + assertEquals(1, ps.executeUpdate()); + ps.close(); + } + + private boolean deleteRow(Connection connection, byte[] id) + throws SQLException { + String sql = "DELETE FROM foo WHERE uniqueId = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + if (id == null) ps.setNull(1, BINARY); + else ps.setBytes(1, id); + int affected = ps.executeUpdate(); + ps.close(); + return affected == 1; + } + + private void insertBatch(Connection connection, byte[][] ids, + String[] names) throws SQLException { + assertEquals(ids.length, names.length); + String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)"; + PreparedStatement ps = connection.prepareStatement(sql); + for (int i = 0; i < ids.length; i++) { + if (ids[i] == null) ps.setNull(1, BINARY); + else ps.setBytes(1, ids[i]); + ps.setString(2, names[i]); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + assertEquals(ids.length, batchAffected.length); + for (int affected : batchAffected) assertEquals(1, affected); + ps.close(); + } + + private void updateBatch(Connection connection, byte[][] ids, + String[] names) throws SQLException { + assertEquals(ids.length, names.length); + String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + for (int i = 0; i < ids.length; i++) { + if (ids[i] == null) ps.setNull(2, BINARY); + else ps.setBytes(2, ids[i]); + ps.setString(1, names[i]); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + assertEquals(ids.length, batchAffected.length); + for (int affected : batchAffected) assertEquals(1, affected); + ps.close(); + } + + private boolean[] deleteBatch(Connection connection, byte[][] ids) + throws SQLException { + String sql = "DELETE FROM foo WHERE uniqueId = ?"; + PreparedStatement ps = connection.prepareStatement(sql); + for (byte[] id : ids) { + if (id == null) ps.setNull(1, BINARY); + else ps.setBytes(1, id); + ps.addBatch(); + } + int[] batchAffected = ps.executeBatch(); + assertEquals(ids.length, batchAffected.length); + boolean[] ret = new boolean[ids.length]; + for (int i = 0; i < batchAffected.length; i++) + ret[i] = batchAffected[i] == 1; + ps.close(); + return ret; + } + + private String getPredecessor(Connection connection, byte[] id) + throws SQLException { + String sql = "SELECT name FROM foo WHERE uniqueId < ?" + + " ORDER BY uniqueId DESC"; + PreparedStatement ps = connection.prepareStatement(sql); + ps.setBytes(1, id); + ps.setMaxRows(1); + ResultSet rs = ps.executeQuery(); + String name = rs.next() ? rs.getString(1) : null; + assertFalse(rs.next()); + rs.close(); + ps.close(); + return name; + } + + private List<String> getNames(Connection connection) throws SQLException { + String sql = "SELECT name FROM foo ORDER BY uniqueId NULLS FIRST"; + List<String> names = new ArrayList<>(); + PreparedStatement ps = connection.prepareStatement(sql); + ResultSet rs = ps.executeQuery(); + while (rs.next()) names.add(rs.getString(1)); + rs.close(); + ps.close(); + return names; + } + + private boolean findSequence(File f, byte[] sequence) throws IOException { + if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) + for (File child : children) + if (findSequence(child, sequence)) return true; + return false; + } else if (f.isFile()) { + FileInputStream in = new FileInputStream(f); + try { + int offset = 0; + while (true) { + int read = in.read(); + if (read == -1) return false; + if (((byte) read) == sequence[offset]) { + offset++; + if (offset == sequence.length) return true; + } else { + offset = 0; + } + } + } finally { + in.close(); + } + } else { + return false; + } + } + + @After + public void tearDown() throws Exception { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java index 5b62ba75071b94b5c8d163f5fa709f9d949d1f58..ebfe9879f2d23106629a5b23264c42a286420209 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicH2Test.java @@ -1,345 +1,47 @@ package org.briarproject.bramble.db; -import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.test.TestUtils; import org.briarproject.bramble.util.StringUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; import java.io.File; import java.sql.Connection; import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.List; +import java.util.Properties; -import static java.sql.Types.BINARY; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +public class BasicH2Test extends BasicDatabaseTest { -public class BasicH2Test extends BrambleTestCase { + private final SecretKey key = TestUtils.getSecretKey(); - private static final String CREATE_TABLE = - "CREATE TABLE foo (uniqueId BINARY(32), name VARCHAR NOT NULL)"; - private static final int BATCH_SIZE = 100; - - private final File testDir = TestUtils.getTestDirectory(); - private final File db = new File(testDir, "db"); - private final String url = "jdbc:h2:" + db.getAbsolutePath(); - - private Connection connection = null; - - @Before - public void setUp() throws Exception { - testDir.mkdirs(); - Class.forName("org.h2.Driver"); - connection = DriverManager.getConnection(url); - } - - @Test - public void testInsertUpdateAndDelete() throws Exception { - // Create the table - createTable(connection); - // Generate an ID and two names - byte[] id = TestUtils.getRandomId(); - String oldName = StringUtils.getRandomString(50); - String newName = StringUtils.getRandomString(50); - // Insert the ID and old name into the table - insertRow(id, oldName); - // Check that the old name can be retrieved using the ID - assertTrue(rowExists(id)); - assertEquals(oldName, getName(id)); - // Update the name - updateRow(id, newName); - // Check that the new name can be retrieved using the ID - assertTrue(rowExists(id)); - assertEquals(newName, getName(id)); - // Delete the row from the table - assertTrue(deleteRow(id)); - // Check that the row no longer exists - assertFalse(rowExists(id)); - // Deleting the row again should have no effect - assertFalse(deleteRow(id)); - } - - @Test - public void testBatchInsertUpdateAndDelete() throws Exception { - // Create the table - createTable(connection); - // Generate some IDs and two sets of names - byte[][] ids = new byte[BATCH_SIZE][]; - String[] oldNames = new String[BATCH_SIZE]; - String[] newNames = new String[BATCH_SIZE]; - for (int i = 0; i < BATCH_SIZE; i++) { - ids[i] = TestUtils.getRandomId(); - oldNames[i] = StringUtils.getRandomString(50); - newNames[i] = StringUtils.getRandomString(50); - } - // Insert the IDs and old names into the table as a batch - insertBatch(ids, oldNames); - // Update the names as a batch - updateBatch(ids, newNames); - // Check that the new names can be retrieved using the IDs - for (int i = 0; i < BATCH_SIZE; i++) { - assertTrue(rowExists(ids[i])); - assertEquals(newNames[i], getName(ids[i])); - } - // Delete the rows as a batch - boolean[] deleted = deleteBatch(ids); - // Check that the rows no longer exist - for (int i = 0; i < BATCH_SIZE; i++) { - assertTrue(deleted[i]); - assertFalse(rowExists(ids[i])); - } - // Deleting the rows again should have no effect - deleted = deleteBatch(ids); - for (int i = 0; i < BATCH_SIZE; i++) assertFalse(deleted[i]); - } - - @Test - public void testSortOrder() throws Exception { - byte[] first = new byte[] { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 - }; - byte[] second = new byte[] { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 127 - }; - byte[] third = new byte[] { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, (byte) 255 - }; - // Create the table - createTable(connection); - // Insert the rows - insertRow(first, "first"); - insertRow(second, "second"); - insertRow(third, "third"); - insertRow(null, "null"); - // Check the ordering of the < operator: the null ID is not comparable - assertNull(getPredecessor(first)); - assertEquals("first", getPredecessor(second)); - assertEquals("second", getPredecessor(third)); - assertNull(getPredecessor(null)); - // Check the ordering of ORDER BY: nulls come first - List<String> names = getNames(); - assertEquals(4, names.size()); - assertEquals("null", names.get(0)); - assertEquals("first", names.get(1)); - assertEquals("second", names.get(2)); - assertEquals("third", names.get(3)); - } - - private void createTable(Connection connection) throws SQLException { - try { - Statement s = connection.createStatement(); - s.executeUpdate(CREATE_TABLE); - s.close(); - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private void insertRow(byte[] id, String name) throws SQLException { - String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - if (id == null) ps.setNull(1, BINARY); - else ps.setBytes(1, id); - ps.setString(2, name); - int affected = ps.executeUpdate(); - assertEquals(1, affected); - ps.close(); - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private boolean rowExists(byte[] id) throws SQLException { - assertNotNull(id); - String sql = "SELECT NULL FROM foo WHERE uniqueID = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - ps.setBytes(1, id); - ResultSet rs = ps.executeQuery(); - boolean found = rs.next(); - assertFalse(rs.next()); - rs.close(); - ps.close(); - return found; - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private String getName(byte[] id) throws SQLException { - assertNotNull(id); - String sql = "SELECT name FROM foo WHERE uniqueID = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - ps.setBytes(1, id); - ResultSet rs = ps.executeQuery(); - assertTrue(rs.next()); - String name = rs.getString(1); - assertFalse(rs.next()); - rs.close(); - ps.close(); - return name; - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private void updateRow(byte[] id, String name) throws SQLException { - String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - if (id == null) ps.setNull(2, BINARY); - else ps.setBytes(2, id); - ps.setString(1, name); - assertEquals(1, ps.executeUpdate()); - ps.close(); - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private boolean deleteRow(byte[] id) throws SQLException { - String sql = "DELETE FROM foo WHERE uniqueId = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - if (id == null) ps.setNull(1, BINARY); - else ps.setBytes(1, id); - int affected = ps.executeUpdate(); - ps.close(); - return affected == 1; - } catch (SQLException e) { - connection.close(); - throw e; - } + @Override + protected String getBinaryType() { + return "BINARY(32)"; } - private void insertBatch(byte[][] ids, String[] names) throws SQLException { - assertEquals(ids.length, names.length); - String sql = "INSERT INTO foo (uniqueId, name) VALUES (?, ?)"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - for (int i = 0; i < ids.length; i++) { - if (ids[i] == null) ps.setNull(1, BINARY); - else ps.setBytes(1, ids[i]); - ps.setString(2, names[i]); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - assertEquals(ids.length, batchAffected.length); - for (int affected : batchAffected) assertEquals(1, affected); - ps.close(); - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private void updateBatch(byte[][] ids, String[] names) throws SQLException { - assertEquals(ids.length, names.length); - String sql = "UPDATE foo SET name = ? WHERE uniqueId = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - for (int i = 0; i < ids.length; i++) { - if (ids[i] == null) ps.setNull(2, BINARY); - else ps.setBytes(2, ids[i]); - ps.setString(1, names[i]); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - assertEquals(ids.length, batchAffected.length); - for (int affected : batchAffected) assertEquals(1, affected); - ps.close(); - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private boolean[] deleteBatch(byte[][] ids) throws SQLException { - String sql = "DELETE FROM foo WHERE uniqueId = ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - for (byte[] id : ids) { - if (id == null) ps.setNull(1, BINARY); - else ps.setBytes(1, id); - ps.addBatch(); - } - int[] batchAffected = ps.executeBatch(); - assertEquals(ids.length, batchAffected.length); - boolean[] ret = new boolean[ids.length]; - for (int i = 0; i < batchAffected.length; i++) - ret[i] = batchAffected[i] == 1; - ps.close(); - return ret; - } catch (SQLException e) { - connection.close(); - throw e; - } - } - - private String getPredecessor(byte[] id) throws SQLException { - String sql = "SELECT name FROM foo WHERE uniqueId < ?" - + " ORDER BY uniqueId DESC LIMIT ?"; - try { - PreparedStatement ps = connection.prepareStatement(sql); - ps.setBytes(1, id); - ps.setInt(2, 1); - ResultSet rs = ps.executeQuery(); - String name = rs.next() ? rs.getString(1) : null; - assertFalse(rs.next()); - rs.close(); - ps.close(); - return name; - } catch (SQLException e) { - connection.close(); - throw e; - } + @Override + protected String getDriverName() { + return "org.h2.Driver"; } - private List<String> getNames() throws SQLException { - String sql = "SELECT name FROM foo ORDER BY uniqueId"; - List<String> names = new ArrayList<>(); - try { - PreparedStatement ps = connection.prepareStatement(sql); - ResultSet rs = ps.executeQuery(); - while (rs.next()) names.add(rs.getString(1)); - rs.close(); - ps.close(); - return names; - } catch (SQLException e) { - connection.close(); - throw e; + @Override + protected Connection openConnection(File db, boolean encrypt) + throws SQLException { + String url = "jdbc:h2:split:" + db.getAbsolutePath() + + ";DB_CLOSE_ON_EXIT=false"; + Properties props = new Properties(); + props.setProperty("user", "user"); + if (encrypt) { + url += ";CIPHER=AES"; + String hex = StringUtils.toHexString(key.getBytes()); + props.setProperty("password", hex + " password"); } + return DriverManager.getConnection(url, props); } - @After - public void tearDown() throws Exception { - if (connection != null) connection.close(); - TestUtils.deleteTestDirectory(testDir); + @Override + protected void shutdownDatabase(File db, boolean encrypt) + throws SQLException { + // The DB is closed automatically when the connection is closed } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fae313cf36f2b9e474c36e92ae8583308692fac5 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/BasicHyperSqlTest.java @@ -0,0 +1,48 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.test.TestUtils; +import org.briarproject.bramble.util.StringUtils; + +import java.io.File; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public class BasicHyperSqlTest extends BasicDatabaseTest { + + private final SecretKey key = TestUtils.getSecretKey(); + + @Override + protected String getBinaryType() { + return "BINARY(32)"; + } + + @Override + protected String getDriverName() { + return "org.hsqldb.jdbc.JDBCDriver"; + } + + @Override + protected Connection openConnection(File db, boolean encrypt) + throws SQLException { + String url = "jdbc:hsqldb:file:" + db.getAbsolutePath() + + ";sql.enforce_size=false;allow_empty_batch=true"; + if (encrypt) { + String hex = StringUtils.toHexString(key.getBytes()); + url += ";encrypt_lobs=true;crypt_type=AES;crypt_key=" + hex; + } + return DriverManager.getConnection(url); + } + + @Override + protected void shutdownDatabase(File db, boolean encrypt) + throws SQLException { + Connection c = openConnection(db, encrypt); + Statement s = c.createStatement(); + s.executeQuery("SHUTDOWN"); + s.close(); + c.close(); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java index 4afea4bf42a6b39b0bcb5a9bea2f6142d999e026..fb0f6e4a3071be2ec9e50c0d6f6147cbf6ed8399 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/H2DatabaseTest.java @@ -1,1686 +1,16 @@ package org.briarproject.bramble.db; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.crypto.SecretKey; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.identity.LocalAuthor; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.settings.Settings; -import org.briarproject.bramble.api.sync.ClientId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.sync.MessageStatus; -import org.briarproject.bramble.api.sync.ValidationManager.State; -import org.briarproject.bramble.api.transport.IncomingKeys; -import org.briarproject.bramble.api.transport.OutgoingKeys; -import org.briarproject.bramble.api.transport.TransportKeys; -import org.briarproject.bramble.system.SystemClock; -import org.briarproject.bramble.test.BrambleTestCase; -import org.briarproject.bramble.test.TestDatabaseConfig; -import org.briarproject.bramble.test.TestUtils; -import org.briarproject.bramble.util.StringUtils; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +import org.briarproject.bramble.api.db.DatabaseConfig; +import org.briarproject.bramble.api.system.Clock; -import java.io.File; -import java.sql.Connection; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Random; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; - -import static java.util.concurrent.TimeUnit.SECONDS; -import static org.briarproject.bramble.api.db.Metadata.REMOVE; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; -import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; -import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; -import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; -import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; -import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED; -import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID; -import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING; -import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class H2DatabaseTest extends BrambleTestCase { - - private static final int ONE_MEGABYTE = 1024 * 1024; - private static final int MAX_SIZE = 5 * ONE_MEGABYTE; - - private final File testDir = TestUtils.getTestDirectory(); - private final GroupId groupId; - private final ClientId clientId; - private final Group group; - private final Author author; - private final AuthorId localAuthorId; - private final LocalAuthor localAuthor; - private final MessageId messageId; - private final long timestamp; - private final int size; - private final byte[] raw; - private final Message message; - private final TransportId transportId; - private final ContactId contactId; +public class H2DatabaseTest extends JdbcDatabaseTest { public H2DatabaseTest() throws Exception { - groupId = new GroupId(TestUtils.getRandomId()); - clientId = new ClientId(StringUtils.getRandomString(5)); - byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; - group = new Group(groupId, clientId, descriptor); - AuthorId authorId = new AuthorId(TestUtils.getRandomId()); - author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); - localAuthorId = new AuthorId(TestUtils.getRandomId()); - timestamp = System.currentTimeMillis(); - localAuthor = new LocalAuthor(localAuthorId, "Bob", - new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp); - messageId = new MessageId(TestUtils.getRandomId()); - size = 1234; - raw = TestUtils.getRandomBytes(size); - message = new Message(messageId, groupId, timestamp, raw); - transportId = new TransportId("id"); - contactId = new ContactId(1); - } - - @Before - public void setUp() { - assertTrue(testDir.mkdirs()); - } - - @Test - public void testPersistence() throws Exception { - // Store some records - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - assertFalse(db.containsContact(txn, contactId)); - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - assertTrue(db.containsContact(txn, contactId)); - assertFalse(db.containsGroup(txn, groupId)); - db.addGroup(txn, group); - assertTrue(db.containsGroup(txn, groupId)); - assertFalse(db.containsMessage(txn, messageId)); - db.addMessage(txn, message, DELIVERED, true); - assertTrue(db.containsMessage(txn, messageId)); - db.commitTransaction(txn); - db.close(); - - // Check that the records are still there - db = open(true); - txn = db.startTransaction(); - assertTrue(db.containsContact(txn, contactId)); - assertTrue(db.containsGroup(txn, groupId)); - assertTrue(db.containsMessage(txn, messageId)); - byte[] raw1 = db.getRawMessage(txn, messageId); - assertArrayEquals(raw, raw1); - - // Delete the records - db.removeMessage(txn, messageId); - db.removeContact(txn, contactId); - db.removeGroup(txn, groupId); - db.commitTransaction(txn); - db.close(); - - // Check that the records are gone - db = open(true); - txn = db.startTransaction(); - assertFalse(db.containsContact(txn, contactId)); - assertFalse(db.containsGroup(txn, groupId)); - assertFalse(db.containsMessage(txn, messageId)); - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testRemovingGroupRemovesMessage() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and a message - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - - // Removing the group should remove the message - assertTrue(db.containsMessage(txn, messageId)); - db.removeGroup(txn, groupId); - assertFalse(db.containsMessage(txn, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - - // The message has no status yet, so it should not be sendable - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Adding a status with seen = false should make the message sendable - db.addStatus(txn, contactId, messageId, false, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertEquals(Collections.singletonList(messageId), ids); - - // Changing the status to seen = true should make the message unsendable - db.raiseSeenFlag(txn, contactId, messageId); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableMessagesMustBeDelivered() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared but unvalidated message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, UNKNOWN, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The message has not been validated, so it should not be sendable - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Marking the message delivered should make it sendable - db.setMessageState(txn, messageId, DELIVERED); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertEquals(Collections.singletonList(messageId), ids); - - // Marking the message invalid should make it unsendable - db.setMessageState(txn, messageId, INVALID); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Marking the message pending should make it unsendable - db.setMessageState(txn, messageId, PENDING); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableMessagesMustHaveSharedGroup() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, an invisible group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The group is invisible, so the message should not be sendable - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Making the group visible should not make the message sendable - db.addGroupVisibility(txn, contactId, groupId, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Sharing the group should make the message sendable - db.setGroupVisibility(txn, contactId, groupId, true); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertEquals(Collections.singletonList(messageId), ids); - - // Unsharing the group should make the message unsendable - db.setGroupVisibility(txn, contactId, groupId, false); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Making the group invisible should make the message unsendable - db.removeGroupVisibility(txn, contactId, groupId); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableMessagesMustBeShared() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and an unshared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, false); - db.addStatus(txn, contactId, messageId, false, false); - - // The message is not shared, so it should not be sendable - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // Sharing the message should make it sendable - db.setMessageShared(txn, messageId); - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertEquals(Collections.singletonList(messageId), ids); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSendableMessagesMustFitCapacity() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The message is sendable, but too large to send - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - size - 1); - assertTrue(ids.isEmpty()); - - // The message is just the right size to send - ids = db.getMessagesToSend(txn, contactId, size); - assertEquals(Collections.singletonList(messageId), ids); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMessagesToAck() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and a visible group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, false); - - // Add some messages to ack - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new Message(messageId1, groupId, timestamp, raw); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, true); - db.raiseAckFlag(txn, contactId, messageId); - db.addMessage(txn, message1, DELIVERED, true); - db.addStatus(txn, contactId, messageId1, false, true); - db.raiseAckFlag(txn, contactId, messageId1); - - // Both message IDs should be returned - Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234); - assertEquals(Arrays.asList(messageId, messageId1), ids); - - // Remove both message IDs - db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1)); - - // Both message IDs should have been removed - assertEquals(Collections.emptyList(), db.getMessagesToAck(txn, - contactId, 1234)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testOutstandingMessageAcked() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // Retrieve the message from the database and mark it as sent - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE); - - // The message should no longer be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - - // Pretend that the message was acked - db.raiseSeenFlag(txn, contactId, messageId); - - // The message still should not be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetFreeSpace() throws Exception { - byte[] largeBody = new byte[MAX_MESSAGE_LENGTH]; - for (int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; - Message message = new Message(messageId, groupId, timestamp, largeBody); - Database<Connection> db = open(false); - - // Sanity check: there should be enough space on disk for this test - assertTrue(testDir.getFreeSpace() > MAX_SIZE); - - // The free space should not be more than the allowed maximum size - long free = db.getFreeSpace(); - assertTrue(free <= MAX_SIZE); - assertTrue(free > 0); - - // Storing a message should reduce the free space - Connection txn = db.startTransaction(); - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.commitTransaction(txn); - assertTrue(db.getFreeSpace() < free); - - db.close(); - } - - @Test - public void testCloseWaitsForCommit() throws Exception { - CountDownLatch closing = new CountDownLatch(1); - CountDownLatch closed = new CountDownLatch(1); - AtomicBoolean transactionFinished = new AtomicBoolean(false); - AtomicBoolean error = new AtomicBoolean(false); - Database<Connection> db = open(false); - - // Start a transaction - Connection txn = db.startTransaction(); - // In another thread, close the database - Thread close = new Thread(() -> { - try { - closing.countDown(); - db.close(); - if (!transactionFinished.get()) error.set(true); - closed.countDown(); - } catch (Exception e) { - error.set(true); - } - }); - close.start(); - closing.await(); - // Do whatever the transaction needs to do - Thread.sleep(10); - transactionFinished.set(true); - // Commit the transaction - db.commitTransaction(txn); - // The other thread should now terminate - assertTrue(closed.await(5, SECONDS)); - // Check that the other thread didn't encounter an error - assertFalse(error.get()); - } - - @Test - public void testCloseWaitsForAbort() throws Exception { - CountDownLatch closing = new CountDownLatch(1); - CountDownLatch closed = new CountDownLatch(1); - AtomicBoolean transactionFinished = new AtomicBoolean(false); - AtomicBoolean error = new AtomicBoolean(false); - Database<Connection> db = open(false); - - // Start a transaction - Connection txn = db.startTransaction(); - // In another thread, close the database - Thread close = new Thread(() -> { - try { - closing.countDown(); - db.close(); - if (!transactionFinished.get()) error.set(true); - closed.countDown(); - } catch (Exception e) { - error.set(true); - } - }); - close.start(); - closing.await(); - // Do whatever the transaction needs to do - Thread.sleep(10); - transactionFinished.set(true); - // Abort the transaction - db.abortTransaction(txn); - // The other thread should now terminate - assertTrue(closed.await(5, SECONDS)); - // Check that the other thread didn't encounter an error - assertFalse(error.get()); - } - - @Test - public void testUpdateSettings() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Store some settings - Settings s = new Settings(); - s.put("foo", "foo"); - s.put("bar", "bar"); - db.mergeSettings(txn, s, "test"); - assertEquals(s, db.getSettings(txn, "test")); - - // Update one of the settings and add another - Settings s1 = new Settings(); - s1.put("bar", "baz"); - s1.put("bam", "bam"); - db.mergeSettings(txn, s1, "test"); - - // Check that the settings were merged - Settings merged = new Settings(); - merged.put("foo", "foo"); - merged.put("bar", "baz"); - merged.put("bam", "bam"); - assertEquals(merged, db.getSettings(txn, "test")); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testContainsVisibleMessageRequiresMessageInDatabase() - throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and a shared group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - - // The message is not in the database - assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testContainsVisibleMessageRequiresGroupInDatabase() - throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - - // The group is not in the database - assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testContainsVisibleMessageRequiresVisibileGroup() - throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a group and a message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The group is not visible - assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGroupVisibility() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact and a group - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - - // The group should not be visible to the contact - assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.emptyList(), - db.getGroupVisibility(txn, groupId)); - - // Make the group visible to the contact - db.addGroupVisibility(txn, contactId, groupId, false); - assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), - db.getGroupVisibility(txn, groupId)); - - // Share the group with the contact - db.setGroupVisibility(txn, contactId, groupId, true); - assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), - db.getGroupVisibility(txn, groupId)); - - // Unshare the group with the contact - db.setGroupVisibility(txn, contactId, groupId, false); - assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.singletonList(contactId), - db.getGroupVisibility(txn, groupId)); - - // Make the group invisible again - db.removeGroupVisibility(txn, contactId, groupId); - assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); - assertEquals(Collections.emptyList(), - db.getGroupVisibility(txn, groupId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testTransportKeys() throws Exception { - TransportKeys keys = createTransportKeys(); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Initially there should be no transport keys in the database - assertEquals(Collections.emptyMap(), - db.getTransportKeys(txn, transportId)); - - // Add the contact, the transport and the transport keys - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addTransport(txn, transportId, 123); - db.addTransportKeys(txn, contactId, keys); - - // Retrieve the transport keys - Map<ContactId, TransportKeys> newKeys = - db.getTransportKeys(txn, transportId); - assertEquals(1, newKeys.size()); - Entry<ContactId, TransportKeys> e = - newKeys.entrySet().iterator().next(); - assertEquals(contactId, e.getKey()); - TransportKeys k = e.getValue(); - assertEquals(transportId, k.getTransportId()); - assertKeysEquals(keys.getPreviousIncomingKeys(), - k.getPreviousIncomingKeys()); - assertKeysEquals(keys.getCurrentIncomingKeys(), - k.getCurrentIncomingKeys()); - assertKeysEquals(keys.getNextIncomingKeys(), - k.getNextIncomingKeys()); - assertKeysEquals(keys.getCurrentOutgoingKeys(), - k.getCurrentOutgoingKeys()); - - // Removing the contact should remove the transport keys - db.removeContact(txn, contactId); - assertEquals(Collections.emptyMap(), - db.getTransportKeys(txn, transportId)); - - db.commitTransaction(txn); - db.close(); - } - - private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) { - assertArrayEquals(expected.getTagKey().getBytes(), - actual.getTagKey().getBytes()); - assertArrayEquals(expected.getHeaderKey().getBytes(), - actual.getHeaderKey().getBytes()); - assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod()); - assertEquals(expected.getWindowBase(), actual.getWindowBase()); - assertArrayEquals(expected.getWindowBitmap(), actual.getWindowBitmap()); - } - - private void assertKeysEquals(OutgoingKeys expected, OutgoingKeys actual) { - assertArrayEquals(expected.getTagKey().getBytes(), - actual.getTagKey().getBytes()); - assertArrayEquals(expected.getHeaderKey().getBytes(), - actual.getHeaderKey().getBytes()); - assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod()); - assertEquals(expected.getStreamCounter(), actual.getStreamCounter()); - } - - @Test - public void testIncrementStreamCounter() throws Exception { - TransportKeys keys = createTransportKeys(); - long rotationPeriod = keys.getCurrentOutgoingKeys().getRotationPeriod(); - long streamCounter = keys.getCurrentOutgoingKeys().getStreamCounter(); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add the contact, transport and transport keys - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addTransport(txn, transportId, 123); - db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys)); - - // Increment the stream counter twice and retrieve the transport keys - db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod); - db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod); - Map<ContactId, TransportKeys> newKeys = - db.getTransportKeys(txn, transportId); - assertEquals(1, newKeys.size()); - Entry<ContactId, TransportKeys> e = - newKeys.entrySet().iterator().next(); - assertEquals(contactId, e.getKey()); - TransportKeys k = e.getValue(); - assertEquals(transportId, k.getTransportId()); - OutgoingKeys outCurr = k.getCurrentOutgoingKeys(); - assertEquals(rotationPeriod, outCurr.getRotationPeriod()); - assertEquals(streamCounter + 2, outCurr.getStreamCounter()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSetReorderingWindow() throws Exception { - TransportKeys keys = createTransportKeys(); - long rotationPeriod = keys.getCurrentIncomingKeys().getRotationPeriod(); - long base = keys.getCurrentIncomingKeys().getWindowBase(); - byte[] bitmap = keys.getCurrentIncomingKeys().getWindowBitmap(); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add the contact, transport and transport keys - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addTransport(txn, transportId, 123); - db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys)); - - // Update the reordering window and retrieve the transport keys - new Random().nextBytes(bitmap); - db.setReorderingWindow(txn, contactId, transportId, rotationPeriod, - base + 1, bitmap); - Map<ContactId, TransportKeys> newKeys = - db.getTransportKeys(txn, transportId); - assertEquals(1, newKeys.size()); - Entry<ContactId, TransportKeys> e = - newKeys.entrySet().iterator().next(); - assertEquals(contactId, e.getKey()); - TransportKeys k = e.getValue(); - assertEquals(transportId, k.getTransportId()); - IncomingKeys inCurr = k.getCurrentIncomingKeys(); - assertEquals(rotationPeriod, inCurr.getRotationPeriod()); - assertEquals(base + 1, inCurr.getWindowBase()); - assertArrayEquals(bitmap, inCurr.getWindowBitmap()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetContactsByAuthorId() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a local author - no contacts should be associated - db.addLocalAuthor(txn, localAuthor); - - // Add a contact associated with the local author - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - - // Ensure contact is returned from database by Author ID - Collection<Contact> contacts = - db.getContactsByAuthorId(txn, author.getId()); - assertEquals(1, contacts.size()); - assertEquals(contactId, contacts.iterator().next().getId()); - - // Ensure no contacts are returned after contact was deleted - db.removeContact(txn, contactId); - contacts = db.getContactsByAuthorId(txn, author.getId()); - assertEquals(0, contacts.size()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetContactsByLocalAuthorId() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a local author - no contacts should be associated - db.addLocalAuthor(txn, localAuthor); - Collection<ContactId> contacts = db.getContacts(txn, localAuthorId); - assertEquals(Collections.emptyList(), contacts); - - // Add a contact associated with the local author - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - contacts = db.getContacts(txn, localAuthorId); - assertEquals(Collections.singletonList(contactId), contacts); - - // Remove the local author - the contact should be removed - db.removeLocalAuthor(txn, localAuthorId); - contacts = db.getContacts(txn, localAuthorId); - assertEquals(Collections.emptyList(), contacts); - assertFalse(db.containsContact(txn, contactId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testOfferedMessages() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact - initially there should be no offered messages - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - assertEquals(0, db.countOfferedMessages(txn, contactId)); - - // Add some offered messages and count them - List<MessageId> ids = new ArrayList<>(); - for (int i = 0; i < 10; i++) { - MessageId m = new MessageId(TestUtils.getRandomId()); - db.addOfferedMessage(txn, contactId, m); - ids.add(m); - } - assertEquals(10, db.countOfferedMessages(txn, contactId)); - - // Remove some of the offered messages and count again - List<MessageId> half = ids.subList(0, 5); - db.removeOfferedMessages(txn, contactId, half); - assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5))); - assertEquals(4, db.countOfferedMessages(txn, contactId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGroupMetadata() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group - db.addGroup(txn, group); - - // Attach some metadata to the group - Metadata metadata = new Metadata(); - metadata.put("foo", new byte[]{'b', 'a', 'r'}); - metadata.put("baz", new byte[]{'b', 'a', 'm'}); - db.mergeGroupMetadata(txn, groupId, metadata); - - // Retrieve the metadata for the group - Metadata retrieved = db.getGroupMetadata(txn, groupId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Update the metadata - metadata.put("foo", REMOVE); - metadata.put("baz", new byte[] {'q', 'u', 'x'}); - db.mergeGroupMetadata(txn, groupId, metadata); - - // Retrieve the metadata again - retrieved = db.getGroupMetadata(txn, groupId); - assertEquals(1, retrieved.size()); - assertFalse(retrieved.containsKey("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMessageMetadata() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and a message - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - - // Attach some metadata to the message - Metadata metadata = new Metadata(); - metadata.put("foo", new byte[]{'b', 'a', 'r'}); - metadata.put("baz", new byte[]{'b', 'a', 'm'}); - db.mergeMessageMetadata(txn, messageId, metadata); - - // Retrieve the metadata for the message - Metadata retrieved = db.getMessageMetadata(txn, messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Retrieve the metadata for the group - Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); - assertEquals(1, all.size()); - assertTrue(all.containsKey(messageId)); - retrieved = all.get(messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Update the metadata - metadata.put("foo", REMOVE); - metadata.put("baz", new byte[] {'q', 'u', 'x'}); - db.mergeMessageMetadata(txn, messageId, metadata); - - // Retrieve the metadata again - retrieved = db.getMessageMetadata(txn, messageId); - assertEquals(1, retrieved.size()); - assertFalse(retrieved.containsKey("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Retrieve the metadata for the group again - all = db.getMessageMetadata(txn, groupId); - assertEquals(1, all.size()); - assertTrue(all.containsKey(messageId)); - retrieved = all.get(messageId); - assertEquals(1, retrieved.size()); - assertFalse(retrieved.containsKey("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Delete the metadata - db.deleteMessageMetadata(txn, messageId); - - // Retrieve the metadata again - retrieved = db.getMessageMetadata(txn, messageId); - assertTrue(retrieved.isEmpty()); - - // Retrieve the metadata for the group again - all = db.getMessageMetadata(txn, groupId); - assertTrue(all.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMessageMetadataOnlyForDeliveredMessages() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and a message - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - - // Attach some metadata to the message - Metadata metadata = new Metadata(); - metadata.put("foo", new byte[]{'b', 'a', 'r'}); - metadata.put("baz", new byte[]{'b', 'a', 'm'}); - db.mergeMessageMetadata(txn, messageId, metadata); - - // Retrieve the metadata for the message - Metadata retrieved = db.getMessageMetadata(txn, messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId); - assertEquals(1, map.size()); - assertTrue(map.get(messageId).containsKey("foo")); - assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo")); - assertTrue(map.get(messageId).containsKey("baz")); - assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz")); - - // No metadata for unknown messages - db.setMessageState(txn, messageId, UNKNOWN); - retrieved = db.getMessageMetadata(txn, messageId); - assertTrue(retrieved.isEmpty()); - map = db.getMessageMetadata(txn, groupId); - assertTrue(map.isEmpty()); - - // No metadata for invalid messages - db.setMessageState(txn, messageId, INVALID); - retrieved = db.getMessageMetadata(txn, messageId); - assertTrue(retrieved.isEmpty()); - map = db.getMessageMetadata(txn, groupId); - assertTrue(map.isEmpty()); - - // No metadata for pending messages - db.setMessageState(txn, messageId, PENDING); - retrieved = db.getMessageMetadata(txn, messageId); - assertTrue(retrieved.isEmpty()); - map = db.getMessageMetadata(txn, groupId); - assertTrue(map.isEmpty()); - - // Validator can get metadata for pending messages - retrieved = db.getMessageMetadataForValidator(txn, messageId); - assertFalse(retrieved.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMetadataQueries() throws Exception { - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new Message(messageId1, groupId, timestamp, raw); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and two messages - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addMessage(txn, message1, DELIVERED, true); - - // Attach some metadata to the messages - Metadata metadata = new Metadata(); - metadata.put("foo", new byte[]{'b', 'a', 'r'}); - metadata.put("baz", new byte[]{'b', 'a', 'm'}); - db.mergeMessageMetadata(txn, messageId, metadata); - Metadata metadata1 = new Metadata(); - metadata1.put("foo", new byte[]{'q', 'u', 'x'}); - db.mergeMessageMetadata(txn, messageId1, metadata1); - - // Retrieve all the metadata for the group - Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); - assertEquals(2, all.size()); - assertTrue(all.containsKey(messageId)); - assertTrue(all.containsKey(messageId1)); - Metadata retrieved = all.get(messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - retrieved = all.get(messageId1); - assertEquals(1, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); - - // Query the metadata with an empty query - Metadata query = new Metadata(); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(2, all.size()); - assertTrue(all.containsKey(messageId)); - assertTrue(all.containsKey(messageId1)); - retrieved = all.get(messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - retrieved = all.get(messageId1); - assertEquals(1, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); - - // Use a single-term query that matches the first message - query = new Metadata(); - query.put("foo", metadata.get("foo")); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertTrue(all.containsKey(messageId)); - retrieved = all.get(messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Use a single-term query that matches the second message - query = new Metadata(); - query.put("foo", metadata1.get("foo")); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertTrue(all.containsKey(messageId1)); - retrieved = all.get(messageId1); - assertEquals(1, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); - - // Use a multi-term query that matches the first message - query = new Metadata(); - query.put("foo", metadata.get("foo")); - query.put("baz", metadata.get("baz")); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertTrue(all.containsKey(messageId)); - retrieved = all.get(messageId); - assertEquals(2, retrieved.size()); - assertTrue(retrieved.containsKey("foo")); - assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); - assertTrue(retrieved.containsKey("baz")); - assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); - - // Use a multi-term query that doesn't match any messages - query = new Metadata(); - query.put("foo", metadata1.get("foo")); - query.put("baz", metadata.get("baz")); - all = db.getMessageMetadata(txn, groupId, query); - assertTrue(all.isEmpty()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception { - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new Message(messageId1, groupId, timestamp, raw); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and two messages - db.addGroup(txn, group); - db.addMessage(txn, message, DELIVERED, true); - db.addMessage(txn, message1, DELIVERED, true); - - // Attach some metadata to the messages - Metadata metadata = new Metadata(); - metadata.put("foo", new byte[]{'b', 'a', 'r'}); - metadata.put("baz", new byte[]{'b', 'a', 'm'}); - db.mergeMessageMetadata(txn, messageId, metadata); - Metadata metadata1 = new Metadata(); - metadata1.put("foo", new byte[]{'b', 'a', 'r'}); - db.mergeMessageMetadata(txn, messageId1, metadata1); - - for (int i = 0; i < 2; i++) { - Metadata query; - if (i == 0) { - // Query the metadata with an empty query - query = new Metadata(); - } else { - // Query for foo - query = new Metadata(); - query.put("foo", new byte[]{'b', 'a', 'r'}); - } - - db.setMessageState(txn, messageId, DELIVERED); - db.setMessageState(txn, messageId1, DELIVERED); - Map<MessageId, Metadata> all = - db.getMessageMetadata(txn, groupId, query); - assertEquals(2, all.size()); - assertMetadataEquals(metadata, all.get(messageId)); - assertMetadataEquals(metadata1, all.get(messageId1)); - - // No metadata for unknown messages - db.setMessageState(txn, messageId, UNKNOWN); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertMetadataEquals(metadata1, all.get(messageId1)); - - // No metadata for invalid messages - db.setMessageState(txn, messageId, INVALID); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertMetadataEquals(metadata1, all.get(messageId1)); - - // No metadata for pending messages - db.setMessageState(txn, messageId, PENDING); - all = db.getMessageMetadata(txn, groupId, query); - assertEquals(1, all.size()); - assertMetadataEquals(metadata1, all.get(messageId1)); - } - - db.commitTransaction(txn); - db.close(); - } - - private void assertMetadataEquals(Metadata m1, Metadata m2) { - assertEquals(m1.keySet(), m2.keySet()); - for (Entry<String, byte[]> e : m1.entrySet()) { - assertArrayEquals(e.getValue(), m2.get(e.getKey())); - } - } - - @Test - public void testMessageDependencies() throws Exception { - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - MessageId messageId2 = new MessageId(TestUtils.getRandomId()); - MessageId messageId3 = new MessageId(TestUtils.getRandomId()); - MessageId messageId4 = new MessageId(TestUtils.getRandomId()); - Message message1 = new Message(messageId1, groupId, timestamp, raw); - Message message2 = new Message(messageId2, groupId, timestamp, raw); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and some messages - db.addGroup(txn, group); - db.addMessage(txn, message, PENDING, true); - db.addMessage(txn, message1, DELIVERED, true); - db.addMessage(txn, message2, INVALID, true); - - // Add dependencies - db.addMessageDependency(txn, groupId, messageId, messageId1); - db.addMessageDependency(txn, groupId, messageId, messageId2); - db.addMessageDependency(txn, groupId, messageId1, messageId3); - db.addMessageDependency(txn, groupId, messageId2, messageId4); - - Map<MessageId, State> dependencies; - - // Retrieve dependencies for root - dependencies = db.getMessageDependencies(txn, messageId); - assertEquals(2, dependencies.size()); - assertEquals(DELIVERED, dependencies.get(messageId1)); - assertEquals(INVALID, dependencies.get(messageId2)); - - // Retrieve dependencies for message 1 - dependencies = db.getMessageDependencies(txn, messageId1); - assertEquals(1, dependencies.size()); - assertEquals(UNKNOWN, dependencies.get(messageId3)); // Missing - - // Retrieve dependencies for message 2 - dependencies = db.getMessageDependencies(txn, messageId2); - assertEquals(1, dependencies.size()); - assertEquals(UNKNOWN, dependencies.get(messageId4)); // Missing - - // Make sure leaves have no dependencies - dependencies = db.getMessageDependencies(txn, messageId3); - assertEquals(0, dependencies.size()); - dependencies = db.getMessageDependencies(txn, messageId4); - assertEquals(0, dependencies.size()); - - Map<MessageId, State> dependents; - - // Root message does not have dependents - dependents = db.getMessageDependents(txn, messageId); - assertEquals(0, dependents.size()); - - // Messages 1 and 2 have the root as a dependent - dependents = db.getMessageDependents(txn, messageId1); - assertEquals(1, dependents.size()); - assertEquals(PENDING, dependents.get(messageId)); - dependents = db.getMessageDependents(txn, messageId2); - assertEquals(1, dependents.size()); - assertEquals(PENDING, dependents.get(messageId)); - - // Message 3 has message 1 as a dependent - dependents = db.getMessageDependents(txn, messageId3); - assertEquals(1, dependents.size()); - assertEquals(DELIVERED, dependents.get(messageId1)); - - // Message 4 has message 2 as a dependent - dependents = db.getMessageDependents(txn, messageId4); - assertEquals(1, dependents.size()); - assertEquals(INVALID, dependents.get(messageId2)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testMessageDependenciesAcrossGroups() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and a message - db.addGroup(txn, group); - db.addMessage(txn, message, PENDING, true); - - // Add a second group - GroupId groupId1 = new GroupId(TestUtils.getRandomId()); - Group group1 = new Group(groupId1, clientId, - TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH)); - db.addGroup(txn, group1); - - // Add a message to the second group - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - Message message1 = new Message(messageId1, groupId1, timestamp, raw); - db.addMessage(txn, message1, DELIVERED, true); - - // Create an ID for a missing message - MessageId messageId2 = new MessageId(TestUtils.getRandomId()); - - // Add another message to the first group - MessageId messageId3 = new MessageId(TestUtils.getRandomId()); - Message message3 = new Message(messageId3, groupId, timestamp, raw); - db.addMessage(txn, message3, DELIVERED, true); - - // Add dependencies between the messages - db.addMessageDependency(txn, groupId, messageId, messageId1); - db.addMessageDependency(txn, groupId, messageId, messageId2); - db.addMessageDependency(txn, groupId, messageId, messageId3); - - // Retrieve the dependencies for the root - Map<MessageId, State> dependencies; - dependencies = db.getMessageDependencies(txn, messageId); - - // The cross-group dependency should have state INVALID - assertEquals(INVALID, dependencies.get(messageId1)); - - // The missing dependency should have state UNKNOWN - assertEquals(UNKNOWN, dependencies.get(messageId2)); - - // The valid dependency should have its real state - assertEquals(DELIVERED, dependencies.get(messageId3)); - - // Retrieve the dependents for the message in the second group - Map<MessageId, State> dependents; - dependents = db.getMessageDependents(txn, messageId1); - - // The cross-group dependent should have its real state - assertEquals(PENDING, dependents.get(messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetPendingMessagesForDelivery() throws Exception { - MessageId mId1 = new MessageId(TestUtils.getRandomId()); - MessageId mId2 = new MessageId(TestUtils.getRandomId()); - MessageId mId3 = new MessageId(TestUtils.getRandomId()); - MessageId mId4 = new MessageId(TestUtils.getRandomId()); - Message m1 = new Message(mId1, groupId, timestamp, raw); - Message m2 = new Message(mId2, groupId, timestamp, raw); - Message m3 = new Message(mId3, groupId, timestamp, raw); - Message m4 = new Message(mId4, groupId, timestamp, raw); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and some messages with different states - db.addGroup(txn, group); - db.addMessage(txn, m1, UNKNOWN, true); - db.addMessage(txn, m2, INVALID, true); - db.addMessage(txn, m3, PENDING, true); - db.addMessage(txn, m4, DELIVERED, true); - - Collection<MessageId> result; - - // Retrieve messages to be validated - result = db.getMessagesToValidate(txn, clientId); - assertEquals(1, result.size()); - assertTrue(result.contains(mId1)); - - // Retrieve pending messages - result = db.getPendingMessages(txn, clientId); - assertEquals(1, result.size()); - assertTrue(result.contains(mId3)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetMessagesToShare() throws Exception { - MessageId mId1 = new MessageId(TestUtils.getRandomId()); - MessageId mId2 = new MessageId(TestUtils.getRandomId()); - MessageId mId3 = new MessageId(TestUtils.getRandomId()); - MessageId mId4 = new MessageId(TestUtils.getRandomId()); - Message m1 = new Message(mId1, groupId, timestamp, raw); - Message m2 = new Message(mId2, groupId, timestamp, raw); - Message m3 = new Message(mId3, groupId, timestamp, raw); - Message m4 = new Message(mId4, groupId, timestamp, raw); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and some messages - db.addGroup(txn, group); - db.addMessage(txn, m1, DELIVERED, true); - db.addMessage(txn, m2, DELIVERED, false); - db.addMessage(txn, m3, DELIVERED, false); - db.addMessage(txn, m4, DELIVERED, true); - - // Introduce dependencies between the messages - db.addMessageDependency(txn, groupId, mId1, mId2); - db.addMessageDependency(txn, groupId, mId3, mId1); - db.addMessageDependency(txn, groupId, mId4, mId3); - - // Retrieve messages to be shared - Collection<MessageId> result = - db.getMessagesToShare(txn, clientId); - assertEquals(2, result.size()); - assertTrue(result.contains(mId2)); - assertTrue(result.contains(mId3)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testGetMessageStatus() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The message should not be sent or seen - MessageStatus status = db.getMessageStatus(txn, contactId, messageId); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertFalse(status.isSent()); - assertFalse(status.isSeen()); - - // The same status should be returned when querying by group - Collection<MessageStatus> statuses = db.getMessageStatus(txn, - contactId, groupId); - assertEquals(1, statuses.size()); - status = statuses.iterator().next(); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertFalse(status.isSent()); - assertFalse(status.isSeen()); - - // Pretend the message was sent to the contact - db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE); - - // The message should be sent but not seen - status = db.getMessageStatus(txn, contactId, messageId); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertTrue(status.isSent()); - assertFalse(status.isSeen()); - - // The same status should be returned when querying by group - statuses = db.getMessageStatus(txn, contactId, groupId); - assertEquals(1, statuses.size()); - status = statuses.iterator().next(); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertTrue(status.isSent()); - assertFalse(status.isSeen()); - - // Pretend the message was acked by the contact - db.raiseSeenFlag(txn, contactId, messageId); - - // The message should be sent and seen - status = db.getMessageStatus(txn, contactId, messageId); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertTrue(status.isSent()); - assertTrue(status.isSeen()); - - // The same status should be returned when querying by group - statuses = db.getMessageStatus(txn, contactId, groupId); - assertEquals(1, statuses.size()); - status = statuses.iterator().next(); - assertEquals(messageId, status.getMessageId()); - assertEquals(contactId, status.getContactId()); - assertTrue(status.isSent()); - assertTrue(status.isSeen()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testDifferentLocalAuthorsCanHaveTheSameContact() - throws Exception { - AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId()); - LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol", - new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp); - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add two local authors - db.addLocalAuthor(txn, localAuthor); - db.addLocalAuthor(txn, localAuthor1); - - // Add the same contact for each local author - ContactId contactId = - db.addContact(txn, author, localAuthorId, true, true); - ContactId contactId1 = - db.addContact(txn, author, localAuthorId1, true, true); - - // The contacts should be distinct - assertNotEquals(contactId, contactId1); - assertEquals(2, db.getContacts(txn).size()); - assertEquals(1, db.getContacts(txn, localAuthorId).size()); - assertEquals(1, db.getContacts(txn, localAuthorId1).size()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testDeleteMessage() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact, a shared group and a shared message - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - db.addGroup(txn, group); - db.addGroupVisibility(txn, contactId, groupId, true); - db.addMessage(txn, message, DELIVERED, true); - db.addStatus(txn, contactId, messageId, false, false); - - // The message should be visible to the contact - assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); - - // The message should be sendable - Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, - ONE_MEGABYTE); - assertEquals(Collections.singletonList(messageId), ids); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertEquals(Collections.singletonList(messageId), ids); - - // The raw message should not be null - assertNotNull(db.getRawMessage(txn, messageId)); - - // Delete the message - db.deleteMessage(txn, messageId); - - // The message should be visible to the contact - assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); - - // The message should not be sendable - ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); - assertTrue(ids.isEmpty()); - ids = db.getMessagesToOffer(txn, contactId, 100); - assertTrue(ids.isEmpty()); - - // The raw message should be null - assertNull(db.getRawMessage(txn, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSetContactActive() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a contact - db.addLocalAuthor(txn, localAuthor); - assertEquals(contactId, db.addContact(txn, author, localAuthorId, - true, true)); - - // The contact should be active - Contact contact = db.getContact(txn, contactId); - assertTrue(contact.isActive()); - - // Set the contact inactive - db.setContactActive(txn, contactId, false); - - // The contact should be inactive - contact = db.getContact(txn, contactId); - assertFalse(contact.isActive()); - - // Set the contact active - db.setContactActive(txn, contactId, true); - - // The contact should be active - contact = db.getContact(txn, contactId); - assertTrue(contact.isActive()); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testSetMessageState() throws Exception { - - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - - // Add a group and a message - db.addGroup(txn, group); - db.addMessage(txn, message, UNKNOWN, false); - - // Walk the message through the validation and delivery states - assertEquals(UNKNOWN, db.getMessageState(txn, messageId)); - db.setMessageState(txn, messageId, INVALID); - assertEquals(INVALID, db.getMessageState(txn, messageId)); - db.setMessageState(txn, messageId, PENDING); - assertEquals(PENDING, db.getMessageState(txn, messageId)); - db.setMessageState(txn, messageId, DELIVERED); - assertEquals(DELIVERED, db.getMessageState(txn, messageId)); - - db.commitTransaction(txn); - db.close(); - } - - @Test - public void testExceptionHandling() throws Exception { - Database<Connection> db = open(false); - Connection txn = db.startTransaction(); - try { - // Ask for a nonexistent message - an exception should be thrown - db.getRawMessage(txn, messageId); - fail(); - } catch (DbException expected) { - // It should be possible to abort the transaction without error - db.abortTransaction(txn); - } - // It should be possible to close the database cleanly - db.close(); - } - - private Database<Connection> open(boolean resume) throws Exception { - Database<Connection> db = new H2Database(new TestDatabaseConfig(testDir, - MAX_SIZE), new SystemClock()); - if (!resume) TestUtils.deleteTestDirectory(testDir); - db.open(); - return db; - } - - private TransportKeys createTransportKeys() { - SecretKey inPrevTagKey = TestUtils.getSecretKey(); - SecretKey inPrevHeaderKey = TestUtils.getSecretKey(); - IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey, - 1, 123, new byte[4]); - SecretKey inCurrTagKey = TestUtils.getSecretKey(); - SecretKey inCurrHeaderKey = TestUtils.getSecretKey(); - IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey, - 2, 234, new byte[4]); - SecretKey inNextTagKey = TestUtils.getSecretKey(); - SecretKey inNextHeaderKey = TestUtils.getSecretKey(); - IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey, - 3, 345, new byte[4]); - SecretKey outCurrTagKey = TestUtils.getSecretKey(); - SecretKey outCurrHeaderKey = TestUtils.getSecretKey(); - OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey, - 2, 456); - return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr); + super(); } - @After - public void tearDown() { - TestUtils.deleteTestDirectory(testDir); + @Override + protected JdbcDatabase createDatabase(DatabaseConfig config, Clock clock) { + return new H2Database(config, clock); } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java similarity index 95% rename from bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java rename to bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java index 6596861cc3fcab926386c5fa8a4a6dc5f6c62021..1192f9dd35b27c26aca5db558a25f0c16799528f 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/db/TransactionIsolationTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/H2TransactionIsolationTest.java @@ -19,7 +19,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -public class TransactionIsolationTest extends BrambleTestCase { +public class H2TransactionIsolationTest extends BrambleTestCase { private static final String DROP_TABLE = "DROP TABLE foo IF EXISTS"; private static final String CREATE_TABLE = "CREATE TABLE foo" @@ -34,10 +34,10 @@ public class TransactionIsolationTest extends BrambleTestCase { private final File testDir = TestUtils.getTestDirectory(); private final File db = new File(testDir, "db"); - private final String withMvcc = "jdbc:h2:" + db.getAbsolutePath() - + ";MV_STORE=TRUE;MVCC=TRUE"; - private final String withoutMvcc = "jdbc:h2:" + db.getAbsolutePath() - + ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1"; + private final String withMvcc = "jdbc:h2:split:" + db.getAbsolutePath() + + ";MV_STORE=TRUE;MVCC=TRUE;DB_CLOSE_ON_EXIT=false"; + private final String withoutMvcc = "jdbc:h2:split:" + db.getAbsolutePath() + + ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1;DB_CLOSE_ON_EXIT=false"; @Before public void setUp() throws Exception { diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..64f666fc426bae11cd8534b635adc331f03de07d --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/HyperSqlDatabaseTest.java @@ -0,0 +1,16 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.db.DatabaseConfig; +import org.briarproject.bramble.api.system.Clock; + +public class HyperSqlDatabaseTest extends JdbcDatabaseTest { + + public HyperSqlDatabaseTest() throws Exception { + super(); + } + + @Override + protected JdbcDatabase createDatabase(DatabaseConfig config, Clock clock) { + return new HyperSqlDatabase(config, clock); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2812eda45a40641d392fba55cd601394c3b694fe --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/db/JdbcDatabaseTest.java @@ -0,0 +1,1691 @@ +package org.briarproject.bramble.db; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.db.DatabaseConfig; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.settings.Settings; +import org.briarproject.bramble.api.sync.ClientId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.MessageStatus; +import org.briarproject.bramble.api.sync.ValidationManager.State; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.transport.IncomingKeys; +import org.briarproject.bramble.api.transport.OutgoingKeys; +import org.briarproject.bramble.api.transport.TransportKeys; +import org.briarproject.bramble.system.SystemClock; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.test.TestDatabaseConfig; +import org.briarproject.bramble.test.TestUtils; +import org.briarproject.bramble.util.StringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.briarproject.bramble.api.db.Metadata.REMOVE; +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.bramble.api.sync.Group.Visibility.INVISIBLE; +import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH; +import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; +import static org.briarproject.bramble.api.sync.ValidationManager.State.DELIVERED; +import static org.briarproject.bramble.api.sync.ValidationManager.State.INVALID; +import static org.briarproject.bramble.api.sync.ValidationManager.State.PENDING; +import static org.briarproject.bramble.api.sync.ValidationManager.State.UNKNOWN; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public abstract class JdbcDatabaseTest extends BrambleTestCase { + + private static final int ONE_MEGABYTE = 1024 * 1024; + private static final int MAX_SIZE = 5 * ONE_MEGABYTE; + + private final File testDir = TestUtils.getTestDirectory(); + private final GroupId groupId; + private final ClientId clientId; + private final Group group; + private final Author author; + private final AuthorId localAuthorId; + private final LocalAuthor localAuthor; + private final MessageId messageId; + private final long timestamp; + private final int size; + private final byte[] raw; + private final Message message; + private final TransportId transportId; + private final ContactId contactId; + + JdbcDatabaseTest() throws Exception { + groupId = new GroupId(TestUtils.getRandomId()); + clientId = new ClientId(StringUtils.getRandomString(5)); + byte[] descriptor = new byte[MAX_GROUP_DESCRIPTOR_LENGTH]; + group = new Group(groupId, clientId, descriptor); + AuthorId authorId = new AuthorId(TestUtils.getRandomId()); + author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]); + localAuthorId = new AuthorId(TestUtils.getRandomId()); + timestamp = System.currentTimeMillis(); + localAuthor = new LocalAuthor(localAuthorId, "Bob", + new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp); + messageId = new MessageId(TestUtils.getRandomId()); + size = 1234; + raw = TestUtils.getRandomBytes(size); + message = new Message(messageId, groupId, timestamp, raw); + transportId = new TransportId("id"); + contactId = new ContactId(1); + } + + protected abstract JdbcDatabase createDatabase(DatabaseConfig config, + Clock clock); + + @Before + public void setUp() { + assertTrue(testDir.mkdirs()); + } + + @Test + public void testPersistence() throws Exception { + // Store some records + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + assertFalse(db.containsContact(txn, contactId)); + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + assertTrue(db.containsContact(txn, contactId)); + assertFalse(db.containsGroup(txn, groupId)); + db.addGroup(txn, group); + assertTrue(db.containsGroup(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + db.addMessage(txn, message, DELIVERED, true); + assertTrue(db.containsMessage(txn, messageId)); + db.commitTransaction(txn); + db.close(); + + // Check that the records are still there + db = open(true); + txn = db.startTransaction(); + assertTrue(db.containsContact(txn, contactId)); + assertTrue(db.containsGroup(txn, groupId)); + assertTrue(db.containsMessage(txn, messageId)); + byte[] raw1 = db.getRawMessage(txn, messageId); + assertArrayEquals(raw, raw1); + + // Delete the records + db.removeMessage(txn, messageId); + db.removeContact(txn, contactId); + db.removeGroup(txn, groupId); + db.commitTransaction(txn); + db.close(); + + // Check that the records are gone + db = open(true); + txn = db.startTransaction(); + assertFalse(db.containsContact(txn, contactId)); + assertFalse(db.containsGroup(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemovingGroupRemovesMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Removing the group should remove the message + assertTrue(db.containsMessage(txn, messageId)); + db.removeGroup(txn, groupId); + assertFalse(db.containsMessage(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableMessagesMustHaveSeenFlagFalse() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, true); + + // The message has no status yet, so it should not be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Adding a status with seen = false should make the message sendable + db.addStatus(txn, contactId, messageId, false, false); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + // Changing the status to seen = true should make the message unsendable + db.raiseSeenFlag(txn, contactId, messageId); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableMessagesMustBeDelivered() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared but unvalidated message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, UNKNOWN, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message has not been validated, so it should not be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message delivered should make it sendable + db.setMessageState(txn, messageId, DELIVERED); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + // Marking the message invalid should make it unsendable + db.setMessageState(txn, messageId, INVALID); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Marking the message pending should make it unsendable + db.setMessageState(txn, messageId, PENDING); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableMessagesMustHaveSharedGroup() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, an invisible group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The group is invisible, so the message should not be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Making the group visible should not make the message sendable + db.addGroupVisibility(txn, contactId, groupId, false); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Sharing the group should make the message sendable + db.setGroupVisibility(txn, contactId, groupId, true); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + // Unsharing the group should make the message unsendable + db.setGroupVisibility(txn, contactId, groupId, false); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Making the group invisible should make the message unsendable + db.removeGroupVisibility(txn, contactId, groupId); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableMessagesMustBeShared() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and an unshared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, false); + db.addStatus(txn, contactId, messageId, false, false); + + // The message is not shared, so it should not be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // Sharing the message should make it sendable + db.setMessageShared(txn, messageId); + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableMessagesMustFitCapacity() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message is sendable, but too large to send + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + size - 1); + assertTrue(ids.isEmpty()); + + // The message is just the right size to send + ids = db.getMessagesToSend(txn, contactId, size); + assertEquals(Collections.singletonList(messageId), ids); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessagesToAck() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and a visible group + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, false); + + // Add some messages to ack + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, true); + db.raiseAckFlag(txn, contactId, messageId); + db.addMessage(txn, message1, DELIVERED, true); + db.addStatus(txn, contactId, messageId1, false, true); + db.raiseAckFlag(txn, contactId, messageId1); + + // Both message IDs should be returned + Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234); + assertEquals(Arrays.asList(messageId, messageId1), ids); + + // Remove both message IDs + db.lowerAckFlag(txn, contactId, Arrays.asList(messageId, messageId1)); + + // Both message IDs should have been removed + assertEquals(Collections.emptyList(), db.getMessagesToAck(txn, + contactId, 1234)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testOutstandingMessageAcked() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // Retrieve the message from the database and mark it as sent + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE); + + // The message should no longer be sendable + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + + // Pretend that the message was acked + db.raiseSeenFlag(txn, contactId, messageId); + + // The message still should not be sendable + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetFreeSpace() throws Exception { + byte[] largeBody = new byte[MAX_MESSAGE_LENGTH]; + for (int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; + Message message = new Message(messageId, groupId, timestamp, largeBody); + Database<Connection> db = open(false); + + // Sanity check: there should be enough space on disk for this test + assertTrue(testDir.getFreeSpace() > MAX_SIZE); + + // The free space should not be more than the allowed maximum size + long free = db.getFreeSpace(); + assertTrue(free <= MAX_SIZE); + assertTrue(free > 0); + + // Storing a message should reduce the free space + Connection txn = db.startTransaction(); + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.commitTransaction(txn); + assertTrue(db.getFreeSpace() < free); + + db.close(); + } + + @Test + public void testCloseWaitsForCommit() throws Exception { + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch closed = new CountDownLatch(1); + AtomicBoolean transactionFinished = new AtomicBoolean(false); + AtomicBoolean error = new AtomicBoolean(false); + Database<Connection> db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread(() -> { + try { + closing.countDown(); + db.close(); + if (!transactionFinished.get()) error.set(true); + closed.countDown(); + } catch (Exception e) { + error.set(true); + } + }); + close.start(); + closing.await(); + // Do whatever the transaction needs to do + Thread.sleep(10); + transactionFinished.set(true); + // Commit the transaction + db.commitTransaction(txn); + // The other thread should now terminate + assertTrue(closed.await(5, SECONDS)); + // Check that the other thread didn't encounter an error + assertFalse(error.get()); + } + + @Test + public void testCloseWaitsForAbort() throws Exception { + CountDownLatch closing = new CountDownLatch(1); + CountDownLatch closed = new CountDownLatch(1); + AtomicBoolean transactionFinished = new AtomicBoolean(false); + AtomicBoolean error = new AtomicBoolean(false); + Database<Connection> db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread(() -> { + try { + closing.countDown(); + db.close(); + if (!transactionFinished.get()) error.set(true); + closed.countDown(); + } catch (Exception e) { + error.set(true); + } + }); + close.start(); + closing.await(); + // Do whatever the transaction needs to do + Thread.sleep(10); + transactionFinished.set(true); + // Abort the transaction + db.abortTransaction(txn); + // The other thread should now terminate + assertTrue(closed.await(5, SECONDS)); + // Check that the other thread didn't encounter an error + assertFalse(error.get()); + } + + @Test + public void testUpdateSettings() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Store some settings + Settings s = new Settings(); + s.put("foo", "foo"); + s.put("bar", "bar"); + db.mergeSettings(txn, s, "test"); + assertEquals(s, db.getSettings(txn, "test")); + + // Update one of the settings and add another + Settings s1 = new Settings(); + s1.put("bar", "baz"); + s1.put("bam", "bam"); + db.mergeSettings(txn, s1, "test"); + + // Check that the settings were merged + Settings merged = new Settings(); + merged.put("foo", "foo"); + merged.put("bar", "baz"); + merged.put("bam", "bam"); + assertEquals(merged, db.getSettings(txn, "test")); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContainsVisibleMessageRequiresMessageInDatabase() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and a shared group + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + + // The message is not in the database + assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContainsVisibleMessageRequiresGroupInDatabase() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + + // The group is not in the database + assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContainsVisibleMessageRequiresVisibileGroup() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a group and a message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The group is not visible + assertFalse(db.containsVisibleMessage(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGroupVisibility() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and a group + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + + // The group should not be visible to the contact + assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); + assertEquals(Collections.emptyList(), + db.getGroupVisibility(txn, groupId)); + + // Make the group visible to the contact + db.addGroupVisibility(txn, contactId, groupId, false); + assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); + assertEquals(Collections.singletonList(contactId), + db.getGroupVisibility(txn, groupId)); + + // Share the group with the contact + db.setGroupVisibility(txn, contactId, groupId, true); + assertEquals(SHARED, db.getGroupVisibility(txn, contactId, groupId)); + assertEquals(Collections.singletonList(contactId), + db.getGroupVisibility(txn, groupId)); + + // Unshare the group with the contact + db.setGroupVisibility(txn, contactId, groupId, false); + assertEquals(VISIBLE, db.getGroupVisibility(txn, contactId, groupId)); + assertEquals(Collections.singletonList(contactId), + db.getGroupVisibility(txn, groupId)); + + // Make the group invisible again + db.removeGroupVisibility(txn, contactId, groupId); + assertEquals(INVISIBLE, db.getGroupVisibility(txn, contactId, groupId)); + assertEquals(Collections.emptyList(), + db.getGroupVisibility(txn, groupId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testTransportKeys() throws Exception { + TransportKeys keys = createTransportKeys(); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Initially there should be no transport keys in the database + assertEquals(Collections.emptyMap(), + db.getTransportKeys(txn, transportId)); + + // Add the contact, the transport and the transport keys + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addTransport(txn, transportId, 123); + db.addTransportKeys(txn, contactId, keys); + + // Retrieve the transport keys + Map<ContactId, TransportKeys> newKeys = + db.getTransportKeys(txn, transportId); + assertEquals(1, newKeys.size()); + Entry<ContactId, TransportKeys> e = + newKeys.entrySet().iterator().next(); + assertEquals(contactId, e.getKey()); + TransportKeys k = e.getValue(); + assertEquals(transportId, k.getTransportId()); + assertKeysEquals(keys.getPreviousIncomingKeys(), + k.getPreviousIncomingKeys()); + assertKeysEquals(keys.getCurrentIncomingKeys(), + k.getCurrentIncomingKeys()); + assertKeysEquals(keys.getNextIncomingKeys(), + k.getNextIncomingKeys()); + assertKeysEquals(keys.getCurrentOutgoingKeys(), + k.getCurrentOutgoingKeys()); + + // Removing the contact should remove the transport keys + db.removeContact(txn, contactId); + assertEquals(Collections.emptyMap(), + db.getTransportKeys(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) { + assertArrayEquals(expected.getTagKey().getBytes(), + actual.getTagKey().getBytes()); + assertArrayEquals(expected.getHeaderKey().getBytes(), + actual.getHeaderKey().getBytes()); + assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod()); + assertEquals(expected.getWindowBase(), actual.getWindowBase()); + assertArrayEquals(expected.getWindowBitmap(), actual.getWindowBitmap()); + } + + private void assertKeysEquals(OutgoingKeys expected, OutgoingKeys actual) { + assertArrayEquals(expected.getTagKey().getBytes(), + actual.getTagKey().getBytes()); + assertArrayEquals(expected.getHeaderKey().getBytes(), + actual.getHeaderKey().getBytes()); + assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod()); + assertEquals(expected.getStreamCounter(), actual.getStreamCounter()); + } + + @Test + public void testIncrementStreamCounter() throws Exception { + TransportKeys keys = createTransportKeys(); + long rotationPeriod = keys.getCurrentOutgoingKeys().getRotationPeriod(); + long streamCounter = keys.getCurrentOutgoingKeys().getStreamCounter(); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact, transport and transport keys + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addTransport(txn, transportId, 123); + db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys)); + + // Increment the stream counter twice and retrieve the transport keys + db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod); + db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod); + Map<ContactId, TransportKeys> newKeys = + db.getTransportKeys(txn, transportId); + assertEquals(1, newKeys.size()); + Entry<ContactId, TransportKeys> e = + newKeys.entrySet().iterator().next(); + assertEquals(contactId, e.getKey()); + TransportKeys k = e.getValue(); + assertEquals(transportId, k.getTransportId()); + OutgoingKeys outCurr = k.getCurrentOutgoingKeys(); + assertEquals(rotationPeriod, outCurr.getRotationPeriod()); + assertEquals(streamCounter + 2, outCurr.getStreamCounter()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetReorderingWindow() throws Exception { + TransportKeys keys = createTransportKeys(); + long rotationPeriod = keys.getCurrentIncomingKeys().getRotationPeriod(); + long base = keys.getCurrentIncomingKeys().getWindowBase(); + byte[] bitmap = keys.getCurrentIncomingKeys().getWindowBitmap(); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact, transport and transport keys + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addTransport(txn, transportId, 123); + db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys)); + + // Update the reordering window and retrieve the transport keys + new Random().nextBytes(bitmap); + db.setReorderingWindow(txn, contactId, transportId, rotationPeriod, + base + 1, bitmap); + Map<ContactId, TransportKeys> newKeys = + db.getTransportKeys(txn, transportId); + assertEquals(1, newKeys.size()); + Entry<ContactId, TransportKeys> e = + newKeys.entrySet().iterator().next(); + assertEquals(contactId, e.getKey()); + TransportKeys k = e.getValue(); + assertEquals(transportId, k.getTransportId()); + IncomingKeys inCurr = k.getCurrentIncomingKeys(); + assertEquals(rotationPeriod, inCurr.getRotationPeriod()); + assertEquals(base + 1, inCurr.getWindowBase()); + assertArrayEquals(bitmap, inCurr.getWindowBitmap()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetContactsByAuthorId() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a local author - no contacts should be associated + db.addLocalAuthor(txn, localAuthor); + + // Add a contact associated with the local author + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + + // Ensure contact is returned from database by Author ID + Collection<Contact> contacts = + db.getContactsByAuthorId(txn, author.getId()); + assertEquals(1, contacts.size()); + assertEquals(contactId, contacts.iterator().next().getId()); + + // Ensure no contacts are returned after contact was deleted + db.removeContact(txn, contactId); + contacts = db.getContactsByAuthorId(txn, author.getId()); + assertEquals(0, contacts.size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetContactsByLocalAuthorId() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a local author - no contacts should be associated + db.addLocalAuthor(txn, localAuthor); + Collection<ContactId> contacts = db.getContacts(txn, localAuthorId); + assertEquals(Collections.emptyList(), contacts); + + // Add a contact associated with the local author + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + contacts = db.getContacts(txn, localAuthorId); + assertEquals(Collections.singletonList(contactId), contacts); + + // Remove the local author - the contact should be removed + db.removeLocalAuthor(txn, localAuthorId); + contacts = db.getContacts(txn, localAuthorId); + assertEquals(Collections.emptyList(), contacts); + assertFalse(db.containsContact(txn, contactId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testOfferedMessages() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact - initially there should be no offered messages + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + assertEquals(0, db.countOfferedMessages(txn, contactId)); + + // Add some offered messages and count them + List<MessageId> ids = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + MessageId m = new MessageId(TestUtils.getRandomId()); + db.addOfferedMessage(txn, contactId, m); + ids.add(m); + } + assertEquals(10, db.countOfferedMessages(txn, contactId)); + + // Remove some of the offered messages and count again + List<MessageId> half = ids.subList(0, 5); + db.removeOfferedMessages(txn, contactId, half); + assertTrue(db.removeOfferedMessage(txn, contactId, ids.get(5))); + assertEquals(4, db.countOfferedMessages(txn, contactId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGroupMetadata() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group + db.addGroup(txn, group); + + // Attach some metadata to the group + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeGroupMetadata(txn, groupId, metadata); + + // Retrieve the metadata for the group + Metadata retrieved = db.getGroupMetadata(txn, groupId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Update the metadata + metadata.put("foo", REMOVE); + metadata.put("baz", new byte[] {'q', 'u', 'x'}); + db.mergeGroupMetadata(txn, groupId, metadata); + + // Retrieve the metadata again + retrieved = db.getGroupMetadata(txn, groupId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageMetadata() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Attach some metadata to the message + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata for the message + Metadata retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Retrieve the metadata for the group + Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Update the metadata + metadata.put("foo", REMOVE); + metadata.put("baz", new byte[] {'q', 'u', 'x'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata again + retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Retrieve the metadata for the group again + all = db.getMessageMetadata(txn, groupId); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(1, retrieved.size()); + assertFalse(retrieved.containsKey("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Delete the metadata + db.deleteMessageMetadata(txn, messageId); + + // Retrieve the metadata again + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + + // Retrieve the metadata for the group again + all = db.getMessageMetadata(txn, groupId); + assertTrue(all.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageMetadataOnlyForDeliveredMessages() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + + // Attach some metadata to the message + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + + // Retrieve the metadata for the message + Metadata retrieved = db.getMessageMetadata(txn, messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + Map<MessageId, Metadata> map = db.getMessageMetadata(txn, groupId); + assertEquals(1, map.size()); + assertTrue(map.get(messageId).containsKey("foo")); + assertArrayEquals(metadata.get("foo"), map.get(messageId).get("foo")); + assertTrue(map.get(messageId).containsKey("baz")); + assertArrayEquals(metadata.get("baz"), map.get(messageId).get("baz")); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + retrieved = db.getMessageMetadata(txn, messageId); + assertTrue(retrieved.isEmpty()); + map = db.getMessageMetadata(txn, groupId); + assertTrue(map.isEmpty()); + + // Validator can get metadata for pending messages + retrieved = db.getMessageMetadataForValidator(txn, messageId); + assertFalse(retrieved.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMetadataQueries() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and two messages + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); + + // Attach some metadata to the messages + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + Metadata metadata1 = new Metadata(); + metadata1.put("foo", new byte[]{'q', 'u', 'x'}); + db.mergeMessageMetadata(txn, messageId1, metadata1); + + // Retrieve all the metadata for the group + Map<MessageId, Metadata> all = db.getMessageMetadata(txn, groupId); + assertEquals(2, all.size()); + assertTrue(all.containsKey(messageId)); + assertTrue(all.containsKey(messageId1)); + Metadata retrieved = all.get(messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Query the metadata with an empty query + Metadata query = new Metadata(); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(2, all.size()); + assertTrue(all.containsKey(messageId)); + assertTrue(all.containsKey(messageId1)); + retrieved = all.get(messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Use a single-term query that matches the first message + query = new Metadata(); + query.put("foo", metadata.get("foo")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Use a single-term query that matches the second message + query = new Metadata(); + query.put("foo", metadata1.get("foo")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId1)); + retrieved = all.get(messageId1); + assertEquals(1, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata1.get("foo"), retrieved.get("foo")); + + // Use a multi-term query that matches the first message + query = new Metadata(); + query.put("foo", metadata.get("foo")); + query.put("baz", metadata.get("baz")); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertTrue(all.containsKey(messageId)); + retrieved = all.get(messageId); + assertEquals(2, retrieved.size()); + assertTrue(retrieved.containsKey("foo")); + assertArrayEquals(metadata.get("foo"), retrieved.get("foo")); + assertTrue(retrieved.containsKey("baz")); + assertArrayEquals(metadata.get("baz"), retrieved.get("baz")); + + // Use a multi-term query that doesn't match any messages + query = new Metadata(); + query.put("foo", metadata1.get("foo")); + query.put("baz", metadata.get("baz")); + all = db.getMessageMetadata(txn, groupId, query); + assertTrue(all.isEmpty()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMetadataQueriesOnlyForDeliveredMessages() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and two messages + db.addGroup(txn, group); + db.addMessage(txn, message, DELIVERED, true); + db.addMessage(txn, message1, DELIVERED, true); + + // Attach some metadata to the messages + Metadata metadata = new Metadata(); + metadata.put("foo", new byte[]{'b', 'a', 'r'}); + metadata.put("baz", new byte[]{'b', 'a', 'm'}); + db.mergeMessageMetadata(txn, messageId, metadata); + Metadata metadata1 = new Metadata(); + metadata1.put("foo", new byte[]{'b', 'a', 'r'}); + db.mergeMessageMetadata(txn, messageId1, metadata1); + + for (int i = 0; i < 2; i++) { + Metadata query; + if (i == 0) { + // Query the metadata with an empty query + query = new Metadata(); + } else { + // Query for foo + query = new Metadata(); + query.put("foo", new byte[]{'b', 'a', 'r'}); + } + + db.setMessageState(txn, messageId, DELIVERED); + db.setMessageState(txn, messageId1, DELIVERED); + Map<MessageId, Metadata> all = + db.getMessageMetadata(txn, groupId, query); + assertEquals(2, all.size()); + assertMetadataEquals(metadata, all.get(messageId)); + assertMetadataEquals(metadata1, all.get(messageId1)); + + // No metadata for unknown messages + db.setMessageState(txn, messageId, UNKNOWN); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertMetadataEquals(metadata1, all.get(messageId1)); + + // No metadata for invalid messages + db.setMessageState(txn, messageId, INVALID); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertMetadataEquals(metadata1, all.get(messageId1)); + + // No metadata for pending messages + db.setMessageState(txn, messageId, PENDING); + all = db.getMessageMetadata(txn, groupId, query); + assertEquals(1, all.size()); + assertMetadataEquals(metadata1, all.get(messageId1)); + } + + db.commitTransaction(txn); + db.close(); + } + + private void assertMetadataEquals(Metadata m1, Metadata m2) { + assertEquals(m1.keySet(), m2.keySet()); + for (Entry<String, byte[]> e : m1.entrySet()) { + assertArrayEquals(e.getValue(), m2.get(e.getKey())); + } + } + + @Test + public void testMessageDependencies() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + MessageId messageId3 = new MessageId(TestUtils.getRandomId()); + MessageId messageId4 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId, timestamp, raw); + Message message2 = new Message(messageId2, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and some messages + db.addGroup(txn, group); + db.addMessage(txn, message, PENDING, true); + db.addMessage(txn, message1, DELIVERED, true); + db.addMessage(txn, message2, INVALID, true); + + // Add dependencies + db.addMessageDependency(txn, groupId, messageId, messageId1); + db.addMessageDependency(txn, groupId, messageId, messageId2); + db.addMessageDependency(txn, groupId, messageId1, messageId3); + db.addMessageDependency(txn, groupId, messageId2, messageId4); + + Map<MessageId, State> dependencies; + + // Retrieve dependencies for root + dependencies = db.getMessageDependencies(txn, messageId); + assertEquals(2, dependencies.size()); + assertEquals(DELIVERED, dependencies.get(messageId1)); + assertEquals(INVALID, dependencies.get(messageId2)); + + // Retrieve dependencies for message 1 + dependencies = db.getMessageDependencies(txn, messageId1); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(messageId3)); // Missing + + // Retrieve dependencies for message 2 + dependencies = db.getMessageDependencies(txn, messageId2); + assertEquals(1, dependencies.size()); + assertEquals(UNKNOWN, dependencies.get(messageId4)); // Missing + + // Make sure leaves have no dependencies + dependencies = db.getMessageDependencies(txn, messageId3); + assertEquals(0, dependencies.size()); + dependencies = db.getMessageDependencies(txn, messageId4); + assertEquals(0, dependencies.size()); + + Map<MessageId, State> dependents; + + // Root message does not have dependents + dependents = db.getMessageDependents(txn, messageId); + assertEquals(0, dependents.size()); + + // Messages 1 and 2 have the root as a dependent + dependents = db.getMessageDependents(txn, messageId1); + assertEquals(1, dependents.size()); + assertEquals(PENDING, dependents.get(messageId)); + dependents = db.getMessageDependents(txn, messageId2); + assertEquals(1, dependents.size()); + assertEquals(PENDING, dependents.get(messageId)); + + // Message 3 has message 1 as a dependent + dependents = db.getMessageDependents(txn, messageId3); + assertEquals(1, dependents.size()); + assertEquals(DELIVERED, dependents.get(messageId1)); + + // Message 4 has message 2 as a dependent + dependents = db.getMessageDependents(txn, messageId4); + assertEquals(1, dependents.size()); + assertEquals(INVALID, dependents.get(messageId2)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMessageDependenciesAcrossGroups() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, PENDING, true); + + // Add a second group + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = new Group(groupId1, clientId, + TestUtils.getRandomBytes(MAX_GROUP_DESCRIPTOR_LENGTH)); + db.addGroup(txn, group1); + + // Add a message to the second group + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new Message(messageId1, groupId1, timestamp, raw); + db.addMessage(txn, message1, DELIVERED, true); + + // Create an ID for a missing message + MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + + // Add another message to the first group + MessageId messageId3 = new MessageId(TestUtils.getRandomId()); + Message message3 = new Message(messageId3, groupId, timestamp, raw); + db.addMessage(txn, message3, DELIVERED, true); + + // Add dependencies between the messages + db.addMessageDependency(txn, groupId, messageId, messageId1); + db.addMessageDependency(txn, groupId, messageId, messageId2); + db.addMessageDependency(txn, groupId, messageId, messageId3); + + // Retrieve the dependencies for the root + Map<MessageId, State> dependencies; + dependencies = db.getMessageDependencies(txn, messageId); + + // The cross-group dependency should have state INVALID + assertEquals(INVALID, dependencies.get(messageId1)); + + // The missing dependency should have state UNKNOWN + assertEquals(UNKNOWN, dependencies.get(messageId2)); + + // The valid dependency should have its real state + assertEquals(DELIVERED, dependencies.get(messageId3)); + + // Retrieve the dependents for the message in the second group + Map<MessageId, State> dependents; + dependents = db.getMessageDependents(txn, messageId1); + + // The cross-group dependent should have its real state + assertEquals(PENDING, dependents.get(messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetPendingMessagesForDelivery() throws Exception { + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + MessageId mId4 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + Message m3 = new Message(mId3, groupId, timestamp, raw); + Message m4 = new Message(mId4, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and some messages with different states + db.addGroup(txn, group); + db.addMessage(txn, m1, UNKNOWN, true); + db.addMessage(txn, m2, INVALID, true); + db.addMessage(txn, m3, PENDING, true); + db.addMessage(txn, m4, DELIVERED, true); + + Collection<MessageId> result; + + // Retrieve messages to be validated + result = db.getMessagesToValidate(txn, clientId); + assertEquals(1, result.size()); + assertTrue(result.contains(mId1)); + + // Retrieve pending messages + result = db.getPendingMessages(txn, clientId); + assertEquals(1, result.size()); + assertTrue(result.contains(mId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessagesToShare() throws Exception { + MessageId mId1 = new MessageId(TestUtils.getRandomId()); + MessageId mId2 = new MessageId(TestUtils.getRandomId()); + MessageId mId3 = new MessageId(TestUtils.getRandomId()); + MessageId mId4 = new MessageId(TestUtils.getRandomId()); + Message m1 = new Message(mId1, groupId, timestamp, raw); + Message m2 = new Message(mId2, groupId, timestamp, raw); + Message m3 = new Message(mId3, groupId, timestamp, raw); + Message m4 = new Message(mId4, groupId, timestamp, raw); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and some messages + db.addGroup(txn, group); + db.addMessage(txn, m1, DELIVERED, true); + db.addMessage(txn, m2, DELIVERED, false); + db.addMessage(txn, m3, DELIVERED, false); + db.addMessage(txn, m4, DELIVERED, true); + + // Introduce dependencies between the messages + db.addMessageDependency(txn, groupId, mId1, mId2); + db.addMessageDependency(txn, groupId, mId3, mId1); + db.addMessageDependency(txn, groupId, mId4, mId3); + + // Retrieve messages to be shared + Collection<MessageId> result = + db.getMessagesToShare(txn, clientId); + assertEquals(2, result.size()); + assertTrue(result.contains(mId2)); + assertTrue(result.contains(mId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageStatus() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message should not be sent or seen + MessageStatus status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertFalse(status.isSent()); + assertFalse(status.isSeen()); + + // The same status should be returned when querying by group + Collection<MessageStatus> statuses = db.getMessageStatus(txn, + contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertFalse(status.isSent()); + assertFalse(status.isSeen()); + + // Pretend the message was sent to the contact + db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE); + + // The message should be sent but not seen + status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertFalse(status.isSeen()); + + // The same status should be returned when querying by group + statuses = db.getMessageStatus(txn, contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertFalse(status.isSeen()); + + // Pretend the message was acked by the contact + db.raiseSeenFlag(txn, contactId, messageId); + + // The message should be sent and seen + status = db.getMessageStatus(txn, contactId, messageId); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertTrue(status.isSeen()); + + // The same status should be returned when querying by group + statuses = db.getMessageStatus(txn, contactId, groupId); + assertEquals(1, statuses.size()); + status = statuses.iterator().next(); + assertEquals(messageId, status.getMessageId()); + assertEquals(contactId, status.getContactId()); + assertTrue(status.isSent()); + assertTrue(status.isSeen()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testDifferentLocalAuthorsCanHaveTheSameContact() + throws Exception { + AuthorId localAuthorId1 = new AuthorId(TestUtils.getRandomId()); + LocalAuthor localAuthor1 = new LocalAuthor(localAuthorId1, "Carol", + new byte[MAX_PUBLIC_KEY_LENGTH], new byte[123], timestamp); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add two local authors + db.addLocalAuthor(txn, localAuthor); + db.addLocalAuthor(txn, localAuthor1); + + // Add the same contact for each local author + ContactId contactId = + db.addContact(txn, author, localAuthorId, true, true); + ContactId contactId1 = + db.addContact(txn, author, localAuthorId1, true, true); + + // The contacts should be distinct + assertNotEquals(contactId, contactId1); + assertEquals(2, db.getContacts(txn).size()); + assertEquals(1, db.getContacts(txn, localAuthorId).size()); + assertEquals(1, db.getContacts(txn, localAuthorId1).size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testDeleteMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, a shared group and a shared message + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + db.addGroup(txn, group); + db.addGroupVisibility(txn, contactId, groupId, true); + db.addMessage(txn, message, DELIVERED, true); + db.addStatus(txn, contactId, messageId, false, false); + + // The message should be visible to the contact + assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); + + // The message should be sendable + Collection<MessageId> ids = db.getMessagesToSend(txn, contactId, + ONE_MEGABYTE); + assertEquals(Collections.singletonList(messageId), ids); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertEquals(Collections.singletonList(messageId), ids); + + // The raw message should not be null + assertNotNull(db.getRawMessage(txn, messageId)); + + // Delete the message + db.deleteMessage(txn, messageId); + + // The message should be visible to the contact + assertTrue(db.containsVisibleMessage(txn, contactId, messageId)); + + // The message should not be sendable + ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE); + assertTrue(ids.isEmpty()); + ids = db.getMessagesToOffer(txn, contactId, 100); + assertTrue(ids.isEmpty()); + + // The raw message should be null + assertNull(db.getRawMessage(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetContactActive() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + db.addLocalAuthor(txn, localAuthor); + assertEquals(contactId, db.addContact(txn, author, localAuthorId, + true, true)); + + // The contact should be active + Contact contact = db.getContact(txn, contactId); + assertTrue(contact.isActive()); + + // Set the contact inactive + db.setContactActive(txn, contactId, false); + + // The contact should be inactive + contact = db.getContact(txn, contactId); + assertFalse(contact.isActive()); + + // Set the contact active + db.setContactActive(txn, contactId, true); + + // The contact should be active + contact = db.getContact(txn, contactId); + assertTrue(contact.isActive()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetMessageState() throws Exception { + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a group and a message + db.addGroup(txn, group); + db.addMessage(txn, message, UNKNOWN, false); + + // Walk the message through the validation and delivery states + assertEquals(UNKNOWN, db.getMessageState(txn, messageId)); + db.setMessageState(txn, messageId, INVALID); + assertEquals(INVALID, db.getMessageState(txn, messageId)); + db.setMessageState(txn, messageId, PENDING); + assertEquals(PENDING, db.getMessageState(txn, messageId)); + db.setMessageState(txn, messageId, DELIVERED); + assertEquals(DELIVERED, db.getMessageState(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testExceptionHandling() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + try { + // Ask for a nonexistent message - an exception should be thrown + db.getRawMessage(txn, messageId); + fail(); + } catch (DbException expected) { + // It should be possible to abort the transaction without error + db.abortTransaction(txn); + } + // It should be possible to close the database cleanly + db.close(); + } + + private Database<Connection> open(boolean resume) throws Exception { + Database<Connection> db = createDatabase( + new TestDatabaseConfig(testDir, MAX_SIZE), new SystemClock()); + if (!resume) TestUtils.deleteTestDirectory(testDir); + db.open(); + return db; + } + + private TransportKeys createTransportKeys() { + SecretKey inPrevTagKey = TestUtils.getSecretKey(); + SecretKey inPrevHeaderKey = TestUtils.getSecretKey(); + IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey, + 1, 123, new byte[4]); + SecretKey inCurrTagKey = TestUtils.getSecretKey(); + SecretKey inCurrHeaderKey = TestUtils.getSecretKey(); + IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey, + 2, 234, new byte[4]); + SecretKey inNextTagKey = TestUtils.getSecretKey(); + SecretKey inNextHeaderKey = TestUtils.getSecretKey(); + IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey, + 3, 345, new byte[4]); + SecretKey outCurrTagKey = TestUtils.getSecretKey(); + SecretKey outCurrHeaderKey = TestUtils.getSecretKey(); + OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey, + 2, 456); + return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +}