diff --git a/bramble-core/build.gradle b/bramble-core/build.gradle
index 59361dc93eedde403d9044802caedaf426094e55..af2d70bb945706b522fb358bc53d5e40c518920a 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"
@@ -45,6 +46,7 @@ dependencyVerification {
'org.bitlet:weupnp:0.1.4:weupnp-0.1.4.jar:88df7e6504929d00bdb832863761385c68ab92af945b04f0770b126270a444fb',
'org.hamcrest:hamcrest-core:1.3:hamcrest-core-1.3.jar:66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9',
'org.hamcrest:hamcrest-library:1.3:hamcrest-library-1.3.jar:711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c',
+ 'org.hsqldb:hsqldb:2.3.5:hsqldb-2.3.5.jar:6676a6977ac98997a80f827ddbd3fe8ca1e0853dad1492512135fd1a222ccfad',
'org.jmock:jmock-junit4:2.8.2:jmock-junit4-2.8.2.jar:f7ee4df4f7bd7b7f1cafad3b99eb74d579f109d5992ff625347352edb55e674c',
'org.jmock:jmock-legacy:2.8.2:jmock-legacy-2.8.2.jar:f2b985a5c08a9edb7f37612330c058809da3f6a6d63ce792426ebf8ff0d6d31b',
'org.jmock:jmock-testjar:2.8.2:jmock-testjar-2.8.2.jar:8900860f72c474e027cf97fe78dcbf154a1aa7fc62b6845c5fb4e4f3c7bc8760',
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..2ee4d680a2d0db4ee210f6bbad1bf9f8f9ce4277 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,21 +22,23 @@ 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();
url = "jdbc:h2:split:" + path + ";CIPHER=AES;MULTI_THREADED=1"
- + ";WRITE_DELAY=0;DB_CLOSE_ON_EXIT=false";
+ + ";WRITE_DELAY=0";
}
@Override
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..7e3e3307bc5b9395e0d0af09949097bb46018cab 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
@@ -68,32 +68,32 @@ import static org.briarproject.bramble.db.ExponentialBackoff.calculateExpiry;
@NotNullByDefault
abstract class JdbcDatabase implements Database<Connection> {
- private static final int SCHEMA_VERSION = 30;
- private static final int MIN_SCHEMA_VERSION = 30;
+ private static final int SCHEMA_VERSION = 31;
+ private static final int MIN_SCHEMA_VERSION = 31;
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,"
+ + " settingKey _STRING NOT NULL,"
+ + " value _STRING NOT NULL,"
+ + " PRIMARY KEY (namespace, settingKey))";
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,"
+ + " metaKey _STRING NOT NULL,"
+ + " value _BINARY NOT NULL,"
+ + " PRIMARY KEY (groupId, metaKey),"
+ " 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,"
+ + " metaKey _STRING NOT NULL,"
+ + " value _BINARY NOT NULL,"
+ + " PRIMARY KEY (messageId, metaKey),"
+ " 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,"
+ + " rotationPeriod 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, rotationPeriod),"
+ " 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,"
+ + " rotationPeriod 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,16 +385,17 @@ 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;
}
@Override
public Connection startTransaction() throws DbException {
- Connection txn = null;
+ Connection txn;
connectionsLock.lock();
try {
if (closed) throw new DbClosedException();
@@ -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)"
+ + " rotationPeriod, 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,"
+ + " rotationPeriod, 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 metaKey = ? 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, metaKey, 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 metaKey, 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 metaKey, 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 metaKey, 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 settingKey, value FROM settings"
+ + " WHERE namespace = ?";
ps = txn.prepareStatement(sql);
ps.setString(1, namespace);
rs = ps.executeQuery();
@@ -1927,10 +1932,11 @@ 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 rotationPeriod, tagKey, headerKey,"
+ + " base, bitmap"
+ " FROM incomingKeys"
+ " WHERE transportId = ?"
- + " ORDER BY contactId, period";
+ + " ORDER BY contactId, rotationPeriod";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
rs = ps.executeQuery();
@@ -1947,10 +1953,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, rotationPeriod, tagKey, headerKey, stream"
+ " FROM outgoingKeys"
+ " WHERE transportId = ?"
- + " ORDER BY contactId, period";
+ + " ORDER BY contactId, rotationPeriod";
ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
rs = ps.executeQuery();
@@ -1987,7 +1993,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 rotationPeriod = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setString(2, t.getString());
@@ -2081,7 +2088,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 metaKey = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, id);
for (String key : removed) {
@@ -2100,7 +2107,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 metaKey = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(2, id);
for (Entry<String, byte[]> e : retained.entrySet()) {
@@ -2117,7 +2124,7 @@ abstract class JdbcDatabase implements Database<Connection> {
}
// Insert any keys that don't already exist
sql = "INSERT INTO " + tableName
- + " (" + columnName + ", key, value)"
+ + " (" + columnName + ", metaKey, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
ps.setBytes(1, id);
@@ -2149,7 +2156,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 settingKey = ?";
ps = txn.prepareStatement(sql);
for (Entry<String, String> e : s.entrySet()) {
ps.setString(1, e.getValue());
@@ -2164,7 +2171,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, settingKey, value)"
+ " VALUES (?, ?, ?)";
ps = txn.prepareStatement(sql);
int updateIndex = 0, inserted = 0;
@@ -2528,7 +2535,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 rotationPeriod = ?";
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..83678facc22f4dc8af7e457492fb8866fc0b1ae9 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,46 @@
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();
+ 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 97%
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..c91690bd9c7b4eb27baee434b475bf3e811aa567 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,9 +34,9 @@ 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()
+ private final String withMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+ ";MV_STORE=TRUE;MVCC=TRUE";
- private final String withoutMvcc = "jdbc:h2:" + db.getAbsolutePath()
+ private final String withoutMvcc = "jdbc:h2:split:" + db.getAbsolutePath()
+ ";MV_STORE=FALSE;MVCC=FALSE;LOCK_MODE=1";
@Before
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);
+ }
+}