diff --git a/briar-tests/.classpath b/briar-tests/.classpath
new file mode 100644
index 000000000..5e92e5c69
--- /dev/null
+++ b/briar-tests/.classpath
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-tests/.gitignore b/briar-tests/.gitignore
new file mode 100644
index 000000000..ba077a403
--- /dev/null
+++ b/briar-tests/.gitignore
@@ -0,0 +1 @@
+bin
diff --git a/briar-tests/.project b/briar-tests/.project
new file mode 100644
index 000000000..fd15c6bba
--- /dev/null
+++ b/briar-tests/.project
@@ -0,0 +1,17 @@
+
+
+ briar-tests
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/briar-tests/libs/hamcrest-core-1.1.jar b/briar-tests/libs/hamcrest-core-1.1.jar
new file mode 100644
index 000000000..5f1d5ce0c
Binary files /dev/null and b/briar-tests/libs/hamcrest-core-1.1.jar differ
diff --git a/briar-tests/libs/hamcrest-library-1.1.jar b/briar-tests/libs/hamcrest-library-1.1.jar
new file mode 100644
index 000000000..40610c9b4
Binary files /dev/null and b/briar-tests/libs/hamcrest-library-1.1.jar differ
diff --git a/briar-tests/libs/jmock-2.5.1.jar b/briar-tests/libs/jmock-2.5.1.jar
new file mode 100644
index 000000000..4415dfbc9
Binary files /dev/null and b/briar-tests/libs/jmock-2.5.1.jar differ
diff --git a/briar-tests/libs/junit-4.9b3.jar b/briar-tests/libs/junit-4.9b3.jar
new file mode 100644
index 000000000..8c784e581
Binary files /dev/null and b/briar-tests/libs/junit-4.9b3.jar differ
diff --git a/briar-tests/src/.gitignore b/briar-tests/src/.gitignore
new file mode 100644
index 000000000..94260a350
--- /dev/null
+++ b/briar-tests/src/.gitignore
@@ -0,0 +1,2 @@
+build
+test.tmp
diff --git a/briar-tests/src/build.xml b/briar-tests/src/build.xml
new file mode 100644
index 000000000..f0bea9546
--- /dev/null
+++ b/briar-tests/src/build.xml
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/briar-tests/src/net/sf/briar/BriarTestCase.java b/briar-tests/src/net/sf/briar/BriarTestCase.java
new file mode 100644
index 000000000..32f496aef
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/BriarTestCase.java
@@ -0,0 +1,20 @@
+package net.sf.briar;
+
+
+import java.lang.Thread.UncaughtExceptionHandler;
+
+import junit.framework.TestCase;
+
+public abstract class BriarTestCase extends TestCase {
+
+ public BriarTestCase() {
+ super();
+ // Ensure exceptions thrown on worker threads cause tests to fail
+ UncaughtExceptionHandler fail = new UncaughtExceptionHandler() {
+ public void uncaughtException(Thread thread, Throwable throwable) {
+ fail();
+ }
+ };
+ Thread.setDefaultUncaughtExceptionHandler(fail);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/LockFairnessTest.java b/briar-tests/src/net/sf/briar/LockFairnessTest.java
new file mode 100644
index 000000000..5560855a0
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/LockFairnessTest.java
@@ -0,0 +1,161 @@
+package net.sf.briar;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.junit.Test;
+
+public class LockFairnessTest extends BriarTestCase {
+
+ @Test
+ public void testReadersCanShareTheLock() throws Exception {
+ // Use a fair lock
+ final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
+ final CountDownLatch firstReaderHasLock = new CountDownLatch(1);
+ final CountDownLatch firstReaderHasFinished = new CountDownLatch(1);
+ final CountDownLatch secondReaderHasLock = new CountDownLatch(1);
+ final CountDownLatch secondReaderHasFinished = new CountDownLatch(1);
+ // First reader
+ Thread first = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Acquire the lock
+ lock.readLock().lock();
+ try {
+ // Allow the second reader to acquire the lock
+ firstReaderHasLock.countDown();
+ // Wait for the second reader to acquire the lock
+ assertTrue(secondReaderHasLock.await(10, SECONDS));
+ } finally {
+ // Release the lock
+ lock.readLock().unlock();
+ }
+ } catch(InterruptedException e) {
+ fail();
+ }
+ firstReaderHasFinished.countDown();
+ }
+ };
+ first.start();
+ // Second reader
+ Thread second = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Wait for the first reader to acquire the lock
+ assertTrue(firstReaderHasLock.await(10, SECONDS));
+ // Acquire the lock
+ lock.readLock().lock();
+ try {
+ // Allow the first reader to release the lock
+ secondReaderHasLock.countDown();
+ } finally {
+ // Release the lock
+ lock.readLock().unlock();
+ }
+ } catch(InterruptedException e) {
+ fail();
+ }
+ secondReaderHasFinished.countDown();
+ }
+ };
+ second.start();
+ // Wait for both readers to finish
+ assertTrue(firstReaderHasFinished.await(10, SECONDS));
+ assertTrue(secondReaderHasFinished.await(10, SECONDS));
+ }
+
+ @Test
+ public void testWritersDoNotStarve() throws Exception {
+ // Use a fair lock
+ final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
+ final CountDownLatch firstReaderHasLock = new CountDownLatch(1);
+ final CountDownLatch firstReaderHasFinished = new CountDownLatch(1);
+ final CountDownLatch secondReaderHasFinished = new CountDownLatch(1);
+ final CountDownLatch writerHasFinished = new CountDownLatch(1);
+ final AtomicBoolean secondReaderHasHeldLock = new AtomicBoolean(false);
+ final AtomicBoolean writerHasHeldLock = new AtomicBoolean(false);
+ // First reader
+ Thread first = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Acquire the lock
+ lock.readLock().lock();
+ try {
+ // Allow the other threads to acquire the lock
+ firstReaderHasLock.countDown();
+ // Wait for both other threads to wait for the lock
+ while(lock.getQueueLength() < 2) Thread.sleep(10);
+ // No other thread should have acquired the lock
+ assertFalse(secondReaderHasHeldLock.get());
+ assertFalse(writerHasHeldLock.get());
+ } finally {
+ // Release the lock
+ lock.readLock().unlock();
+ }
+ } catch(InterruptedException e) {
+ fail();
+ }
+ firstReaderHasFinished.countDown();
+ }
+ };
+ first.start();
+ // Writer
+ Thread writer = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Wait for the first reader to acquire the lock
+ assertTrue(firstReaderHasLock.await(10, SECONDS));
+ // Acquire the lock
+ lock.writeLock().lock();
+ try {
+ writerHasHeldLock.set(true);
+ // The second reader should not overtake the writer
+ assertFalse(secondReaderHasHeldLock.get());
+ } finally {
+ lock.writeLock().unlock();
+ }
+ } catch(InterruptedException e) {
+ fail();
+ }
+ writerHasFinished.countDown();
+ }
+ };
+ writer.start();
+ // Second reader
+ Thread second = new Thread() {
+ @Override
+ public void run() {
+ try {
+ // Wait for the first reader to acquire the lock
+ assertTrue(firstReaderHasLock.await(10, SECONDS));
+ // Wait for the writer to wait for the lock
+ while(lock.getQueueLength() < 1) Thread.sleep(10);
+ // Acquire the lock
+ lock.readLock().lock();
+ try {
+ secondReaderHasHeldLock.set(true);
+ // The second reader should not overtake the writer
+ assertTrue(writerHasHeldLock.get());
+ } finally {
+ lock.readLock().unlock();
+ }
+ } catch(InterruptedException e) {
+ fail();
+ }
+ secondReaderHasFinished.countDown();
+ }
+ };
+ second.start();
+ // Wait for all the threads to finish
+ assertTrue(firstReaderHasFinished.await(10, SECONDS));
+ assertTrue(secondReaderHasFinished.await(10, SECONDS));
+ assertTrue(writerHasFinished.await(10, SECONDS));
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java
new file mode 100644
index 000000000..bbd7af2f9
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/ProtocolIntegrationTest.java
@@ -0,0 +1,264 @@
+package net.sf.briar;
+
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+import static org.junit.Assert.assertArrayEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Random;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.protocol.Ack;
+import net.sf.briar.api.protocol.Author;
+import net.sf.briar.api.protocol.AuthorFactory;
+import net.sf.briar.api.protocol.Batch;
+import net.sf.briar.api.protocol.BatchId;
+import net.sf.briar.api.protocol.Group;
+import net.sf.briar.api.protocol.GroupFactory;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageFactory;
+import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.Offer;
+import net.sf.briar.api.protocol.PacketFactory;
+import net.sf.briar.api.protocol.ProtocolReader;
+import net.sf.briar.api.protocol.ProtocolReaderFactory;
+import net.sf.briar.api.protocol.ProtocolWriter;
+import net.sf.briar.api.protocol.ProtocolWriterFactory;
+import net.sf.briar.api.protocol.RawBatch;
+import net.sf.briar.api.protocol.Request;
+import net.sf.briar.api.protocol.SubscriptionUpdate;
+import net.sf.briar.api.protocol.Transport;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.transport.ConnectionContext;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.api.transport.ConnectionReaderFactory;
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.api.transport.ConnectionWriterFactory;
+import net.sf.briar.clock.ClockModule;
+import net.sf.briar.crypto.CryptoModule;
+import net.sf.briar.db.DatabaseModule;
+import net.sf.briar.lifecycle.LifecycleModule;
+import net.sf.briar.protocol.ProtocolModule;
+import net.sf.briar.protocol.duplex.DuplexProtocolModule;
+import net.sf.briar.protocol.simplex.SimplexProtocolModule;
+import net.sf.briar.serial.SerialModule;
+import net.sf.briar.transport.TransportModule;
+
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class ProtocolIntegrationTest extends BriarTestCase {
+
+ private final BatchId ack = new BatchId(TestUtils.getRandomId());
+ private final long timestamp = System.currentTimeMillis();
+
+ private final ConnectionReaderFactory connectionReaderFactory;
+ private final ConnectionWriterFactory connectionWriterFactory;
+ private final ProtocolReaderFactory protocolReaderFactory;
+ private final ProtocolWriterFactory protocolWriterFactory;
+ private final PacketFactory packetFactory;
+ private final CryptoComponent crypto;
+ private final ContactId contactId;
+ private final TransportId transportId;
+ private final byte[] secret;
+ private final Author author;
+ private final Group group, group1;
+ private final Message message, message1, message2, message3;
+ private final String authorName = "Alice";
+ private final String subject = "Hello";
+ private final String messageBody = "Hello world";
+ private final Collection transports;
+
+ public ProtocolIntegrationTest() throws Exception {
+ super();
+ Injector i = Guice.createInjector(new ClockModule(), new CryptoModule(),
+ new DatabaseModule(), new LifecycleModule(),
+ new ProtocolModule(), new SerialModule(),
+ new TestDatabaseModule(), new SimplexProtocolModule(),
+ new TransportModule(), new DuplexProtocolModule());
+ connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class);
+ connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
+ protocolReaderFactory = i.getInstance(ProtocolReaderFactory.class);
+ protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class);
+ packetFactory = i.getInstance(PacketFactory.class);
+ crypto = i.getInstance(CryptoComponent.class);
+ contactId = new ContactId(234);
+ transportId = new TransportId(TestUtils.getRandomId());
+ // Create a shared secret
+ Random r = new Random();
+ secret = new byte[32];
+ r.nextBytes(secret);
+ // Create two groups: one restricted, one unrestricted
+ GroupFactory groupFactory = i.getInstance(GroupFactory.class);
+ group = groupFactory.createGroup("Unrestricted group", null);
+ KeyPair groupKeyPair = crypto.generateSignatureKeyPair();
+ group1 = groupFactory.createGroup("Restricted group",
+ groupKeyPair.getPublic().getEncoded());
+ // Create an author
+ AuthorFactory authorFactory = i.getInstance(AuthorFactory.class);
+ KeyPair authorKeyPair = crypto.generateSignatureKeyPair();
+ author = authorFactory.createAuthor(authorName,
+ authorKeyPair.getPublic().getEncoded());
+ // Create two messages to each group: one anonymous, one pseudonymous
+ MessageFactory messageFactory = i.getInstance(MessageFactory.class);
+ message = messageFactory.createMessage(null, group, subject,
+ messageBody.getBytes("UTF-8"));
+ message1 = messageFactory.createMessage(null, group1,
+ groupKeyPair.getPrivate(), subject,
+ messageBody.getBytes("UTF-8"));
+ message2 = messageFactory.createMessage(null, group, author,
+ authorKeyPair.getPrivate(), subject,
+ messageBody.getBytes("UTF-8"));
+ message3 = messageFactory.createMessage(null, group1,
+ groupKeyPair.getPrivate(), author, authorKeyPair.getPrivate(),
+ subject, messageBody.getBytes("UTF-8"));
+ // Create some transports
+ TransportId transportId = new TransportId(TestUtils.getRandomId());
+ Transport transport = new Transport(transportId,
+ Collections.singletonMap("bar", "baz"));
+ transports = Collections.singletonList(transport);
+ }
+
+ @Test
+ public void testWriteAndRead() throws Exception {
+ read(write());
+ }
+
+ private byte[] write() throws Exception {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+ secret.clone(), 0L, true);
+ ConnectionWriter conn = connectionWriterFactory.createConnectionWriter(
+ out, Long.MAX_VALUE, ctx, false, true);
+ OutputStream out1 = conn.getOutputStream();
+ ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out1,
+ false);
+
+ Ack a = packetFactory.createAck(Collections.singletonList(ack));
+ writer.writeAck(a);
+
+ Collection batch = Arrays.asList(message.getSerialised(),
+ message1.getSerialised(), message2.getSerialised(),
+ message3.getSerialised());
+ RawBatch b = packetFactory.createBatch(batch);
+ writer.writeBatch(b);
+
+ Collection offer = Arrays.asList(message.getId(),
+ message1.getId(), message2.getId(), message3.getId());
+ Offer o = packetFactory.createOffer(offer);
+ writer.writeOffer(o);
+
+ BitSet requested = new BitSet(4);
+ requested.set(1);
+ requested.set(3);
+ Request r = packetFactory.createRequest(requested, 4);
+ writer.writeRequest(r);
+
+ // Use a LinkedHashMap for predictable iteration order
+ Map subs = new LinkedHashMap();
+ subs.put(group, 0L);
+ subs.put(group1, 0L);
+ SubscriptionUpdate s = packetFactory.createSubscriptionUpdate(
+ Collections.emptyMap(), subs, 0L, timestamp);
+ writer.writeSubscriptionUpdate(s);
+
+ TransportUpdate t = packetFactory.createTransportUpdate(transports,
+ timestamp);
+ writer.writeTransportUpdate(t);
+
+ writer.flush();
+ return out.toByteArray();
+ }
+
+ private void read(byte[] connectionData) throws Exception {
+ InputStream in = new ByteArrayInputStream(connectionData);
+ byte[] tag = new byte[TAG_LENGTH];
+ assertEquals(TAG_LENGTH, in.read(tag, 0, TAG_LENGTH));
+ // FIXME: Check that the expected tag was received
+ ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+ secret.clone(), 0L, false);
+ ConnectionReader conn = connectionReaderFactory.createConnectionReader(
+ in, ctx, true, true);
+ InputStream in1 = conn.getInputStream();
+ ProtocolReader reader = protocolReaderFactory.createProtocolReader(in1);
+
+ // Read the ack
+ assertTrue(reader.hasAck());
+ Ack a = reader.readAck();
+ assertEquals(Collections.singletonList(ack), a.getBatchIds());
+
+ // Read and verify the batch
+ assertTrue(reader.hasBatch());
+ Batch b = reader.readBatch().verify();
+ Collection messages = b.getMessages();
+ assertEquals(4, messages.size());
+ Iterator it = messages.iterator();
+ checkMessageEquality(message, it.next());
+ checkMessageEquality(message1, it.next());
+ checkMessageEquality(message2, it.next());
+ checkMessageEquality(message3, it.next());
+
+ // Read the offer
+ assertTrue(reader.hasOffer());
+ Offer o = reader.readOffer();
+ Collection offered = o.getMessageIds();
+ assertEquals(4, offered.size());
+ Iterator it1 = offered.iterator();
+ assertEquals(message.getId(), it1.next());
+ assertEquals(message1.getId(), it1.next());
+ assertEquals(message2.getId(), it1.next());
+ assertEquals(message3.getId(), it1.next());
+
+ // Read the request
+ assertTrue(reader.hasRequest());
+ Request req = reader.readRequest();
+ BitSet requested = req.getBitmap();
+ assertFalse(requested.get(0));
+ assertTrue(requested.get(1));
+ assertFalse(requested.get(2));
+ assertTrue(requested.get(3));
+ // If there are any padding bits, they should all be zero
+ assertEquals(2, requested.cardinality());
+
+ // Read the subscription update
+ assertTrue(reader.hasSubscriptionUpdate());
+ SubscriptionUpdate s = reader.readSubscriptionUpdate();
+ Map subs = s.getSubscriptions();
+ assertEquals(2, subs.size());
+ assertEquals(Long.valueOf(0L), subs.get(group));
+ assertEquals(Long.valueOf(0L), subs.get(group1));
+ assertTrue(s.getTimestamp() == timestamp);
+
+ // Read the transport update
+ assertTrue(reader.hasTransportUpdate());
+ TransportUpdate t = reader.readTransportUpdate();
+ assertEquals(transports, t.getTransports());
+ assertTrue(t.getTimestamp() == timestamp);
+
+ in.close();
+ }
+
+ private void checkMessageEquality(Message m1, Message m2) {
+ assertEquals(m1.getId(), m2.getId());
+ assertEquals(m1.getParent(), m2.getParent());
+ assertEquals(m1.getGroup(), m2.getGroup());
+ assertEquals(m1.getAuthor(), m2.getAuthor());
+ assertEquals(m1.getTimestamp(), m2.getTimestamp());
+ assertArrayEquals(m1.getSerialised(), m2.getSerialised());
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/TestDatabaseConfig.java b/briar-tests/src/net/sf/briar/TestDatabaseConfig.java
new file mode 100644
index 000000000..fdfedb413
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/TestDatabaseConfig.java
@@ -0,0 +1,33 @@
+package net.sf.briar;
+
+import java.io.File;
+
+import net.sf.briar.api.crypto.Password;
+import net.sf.briar.api.db.DatabaseConfig;
+
+public class TestDatabaseConfig implements DatabaseConfig {
+
+ private final File dir;
+ private final long maxSize;
+
+ public TestDatabaseConfig(File dir, long maxSize) {
+ this.dir = dir;
+ this.maxSize = maxSize;
+ }
+
+ public File getDataDirectory() {
+ return dir;
+ }
+
+ public Password getPassword() {
+ return new Password() {
+ public char[] getPassword() {
+ return "foo bar".toCharArray();
+ }
+ };
+ }
+
+ public long getMaxSize() {
+ return maxSize;
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/TestDatabaseModule.java b/briar-tests/src/net/sf/briar/TestDatabaseModule.java
new file mode 100644
index 000000000..5479d9c6b
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/TestDatabaseModule.java
@@ -0,0 +1,29 @@
+package net.sf.briar;
+
+import java.io.File;
+
+import net.sf.briar.api.db.DatabaseConfig;
+
+import com.google.inject.AbstractModule;
+
+public class TestDatabaseModule extends AbstractModule {
+
+ private final DatabaseConfig config;
+
+ public TestDatabaseModule() {
+ this(new File("."), Long.MAX_VALUE);
+ }
+
+ public TestDatabaseModule(File dir) {
+ this(dir, Long.MAX_VALUE);
+ }
+
+ public TestDatabaseModule(File dir, long maxSize) {
+ this.config = new TestDatabaseConfig(dir, maxSize);
+ }
+
+ @Override
+ protected void configure() {
+ bind(DatabaseConfig.class).toInstance(config);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/TestUtils.java b/briar-tests/src/net/sf/briar/TestUtils.java
new file mode 100644
index 000000000..e5c4f6cfe
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/TestUtils.java
@@ -0,0 +1,76 @@
+package net.sf.briar;
+
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import junit.framework.TestCase;
+import net.sf.briar.api.protocol.UniqueId;
+
+public class TestUtils {
+
+ private static final AtomicInteger nextTestDir =
+ new AtomicInteger((int) (Math.random() * 1000 * 1000));
+ private static final Random random = new Random();
+
+ public static void delete(File f) {
+ if(f.isDirectory()) for(File child : f.listFiles()) delete(child);
+ f.delete();
+ }
+
+ public static void createFile(File f, String s) throws IOException {
+ f.getParentFile().mkdirs();
+ PrintStream out = new PrintStream(new FileOutputStream(f));
+ out.print(s);
+ out.flush();
+ out.close();
+ }
+
+ public static File getTestDirectory() {
+ int name = nextTestDir.getAndIncrement();
+ File testDir = new File("test.tmp/" + name);
+ return testDir;
+ }
+
+ public static void deleteTestDirectory(File testDir) {
+ delete(testDir);
+ testDir.getParentFile().delete(); // Delete if empty
+ }
+
+ public static File getBuildDirectory() {
+ File build = new File("build"); // Ant
+ if(build.exists() && build.isDirectory()) return build;
+ File bin = new File("bin"); // Eclipse
+ if(bin.exists() && bin.isDirectory()) return bin;
+ throw new RuntimeException("Could not find build directory");
+ }
+
+ public static File getFontDirectory() {
+ File f = new File("i18n");
+ if(f.exists() && f.isDirectory()) return f;
+ f = new File("../i18n");
+ if(f.exists() && f.isDirectory()) return f;
+ throw new RuntimeException("Could not find font directory");
+ }
+
+ public static byte[] getRandomId() {
+ byte[] b = new byte[UniqueId.LENGTH];
+ random.nextBytes(b);
+ return b;
+ }
+
+ public static void readFully(InputStream in, byte[] b) throws IOException {
+ int offset = 0;
+ while(offset < b.length) {
+ int read = in.read(b, offset, b.length - offset);
+ if(read == -1) break;
+ offset += read;
+ }
+ TestCase.assertEquals(b.length, offset);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java b/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java
new file mode 100644
index 000000000..96cde2001
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/crypto/CounterModeTest.java
@@ -0,0 +1,156 @@
+package net.sf.briar.crypto;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.Bytes;
+
+import org.junit.Test;
+import org.spongycastle.jce.provider.BouncyCastleProvider;
+
+public class CounterModeTest extends BriarTestCase {
+
+ private static final String CIPHER_ALGO = "AES";
+ private static final String CIPHER_MODE = "AES/CTR/NoPadding";
+ private static final String PROVIDER = "SC";
+ private static final int KEY_SIZE_BYTES = 32; // AES-256
+ private static final int BLOCK_SIZE_BYTES = 16;
+
+ private final SecureRandom random;
+ private final byte[] keyBytes;
+ private final SecretKeySpec key;
+
+ public CounterModeTest() {
+ super();
+ Security.addProvider(new BouncyCastleProvider());
+ random = new SecureRandom();
+ keyBytes = new byte[KEY_SIZE_BYTES];
+ random.nextBytes(keyBytes);
+ key = new SecretKeySpec(keyBytes, CIPHER_ALGO);
+ }
+
+ @Test
+ public void testEveryBitOfIvIsSignificant()
+ throws GeneralSecurityException {
+ // Set each bit of the IV in turn, encrypt the same plaintext and check
+ // that all the resulting ciphertexts are distinct
+ byte[] plaintext = new byte[BLOCK_SIZE_BYTES];
+ random.nextBytes(plaintext);
+ Set ciphertexts = new HashSet();
+ for(int i = 0; i < BLOCK_SIZE_BYTES * 8; i++) {
+ // Set the i^th bit of the IV
+ byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
+ ivBytes[i / 8] |= (byte) (128 >> i % 8);
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ // Encrypt the plaintext
+ Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext =
+ new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
+ ciphertexts.add(new Bytes(ciphertext));
+ }
+ // All the ciphertexts should be distinct using Arrays.equals()
+ assertEquals(BLOCK_SIZE_BYTES * 8, ciphertexts.size());
+ }
+
+ @Test
+ public void testRepeatedIvsProduceRepeatedCiphertexts()
+ throws GeneralSecurityException {
+ // This is the inverse of the previous test, to check that the
+ // distinct ciphertexts were due to using distinct IVs
+ byte[] plaintext = new byte[BLOCK_SIZE_BYTES];
+ random.nextBytes(plaintext);
+ byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
+ random.nextBytes(ivBytes);
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ Set ciphertexts = new HashSet();
+ for(int i = 0; i < BLOCK_SIZE_BYTES * 8; i++) {
+ Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext =
+ new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
+ ciphertexts.add(new Bytes(ciphertext));
+ }
+ assertEquals(1, ciphertexts.size());
+ }
+
+ @Test
+ public void testLeastSignificantBitsUsedAsCounter()
+ throws GeneralSecurityException {
+ // Initialise the least significant 16 bits of the IV to zero and
+ // encrypt ten blocks of zeroes
+ byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10];
+ byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
+ random.nextBytes(ivBytes);
+ ivBytes[BLOCK_SIZE_BYTES - 2] = 0;
+ ivBytes[BLOCK_SIZE_BYTES - 1] = 0;
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
+ // Make sure the IV array hasn't been modified
+ assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 2]);
+ assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 1]);
+ // Initialise the least significant 16 bits of the IV to one and
+ // encrypt another ten blocks of zeroes
+ ivBytes[BLOCK_SIZE_BYTES - 1] = 1;
+ iv = new IvParameterSpec(ivBytes);
+ cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1);
+ // The last nine blocks of the first ciphertext should be identical to
+ // the first nine blocks of the second ciphertext
+ for(int i = 0; i < BLOCK_SIZE_BYTES * 9; i++) {
+ assertEquals(ciphertext[i + BLOCK_SIZE_BYTES], ciphertext1[i]);
+ }
+ }
+
+ @Test
+ public void testCounterUsesMoreThan16Bits()
+ throws GeneralSecurityException {
+ // Initialise the least significant bits of the IV to 2^16-1 and
+ // encrypt ten blocks of zeroes
+ byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10];
+ byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
+ random.nextBytes(ivBytes);
+ ivBytes[BLOCK_SIZE_BYTES - 3] = 0;
+ ivBytes[BLOCK_SIZE_BYTES - 2] = (byte) 255;
+ ivBytes[BLOCK_SIZE_BYTES - 1] = (byte) 255;
+ IvParameterSpec iv = new IvParameterSpec(ivBytes);
+ Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
+ // Make sure the IV array hasn't been modified
+ assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 3]);
+ assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 2]);
+ assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 1]);
+ // Initialise the least significant bits of the IV to 2^16 and
+ // encrypt another ten blocks of zeroes
+ ivBytes[BLOCK_SIZE_BYTES - 3] = 1;
+ ivBytes[BLOCK_SIZE_BYTES - 2] = 0;
+ ivBytes[BLOCK_SIZE_BYTES - 1] = 0;
+ iv = new IvParameterSpec(ivBytes);
+ cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
+ cipher.init(Cipher.ENCRYPT_MODE, key, iv);
+ byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)];
+ cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1);
+ // The last nine blocks of the first ciphertext should be identical to
+ // the first nine blocks of the second ciphertext
+ for(int i = 0; i < BLOCK_SIZE_BYTES * 9; i++) {
+ assertEquals(ciphertext[i + BLOCK_SIZE_BYTES], ciphertext1[i]);
+ }
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java b/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java
new file mode 100644
index 000000000..eb448a550
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/crypto/ErasableKeyTest.java
@@ -0,0 +1,79 @@
+package net.sf.briar.crypto;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.crypto.ErasableKey;
+
+import org.junit.Test;
+
+public class ErasableKeyTest extends BriarTestCase {
+
+ private static final String CIPHER = "AES";
+ private static final String CIPHER_MODE = "AES/CTR/NoPadding";
+ private static final int IV_BYTES = 16; // 128 bits
+ private static final int KEY_BYTES = 32; // 256 bits
+ private static final String MAC = "HMacSHA384";
+
+ private final Random random = new Random();
+
+ @Test
+ public void testCopiesAreErased() {
+ byte[] master = new byte[KEY_BYTES];
+ random.nextBytes(master);
+ ErasableKey k = new ErasableKeyImpl(master, CIPHER);
+ byte[] copy = k.getEncoded();
+ assertArrayEquals(master, copy);
+ k.erase();
+ byte[] blank = new byte[KEY_BYTES];
+ assertArrayEquals(blank, master);
+ assertArrayEquals(blank, copy);
+ }
+
+ @Test
+ public void testErasureDoesNotAffectCipher() throws Exception {
+ byte[] key = new byte[KEY_BYTES];
+ random.nextBytes(key);
+ ErasableKey k = new ErasableKeyImpl(key, CIPHER);
+ Cipher c = Cipher.getInstance(CIPHER_MODE);
+ IvParameterSpec iv = new IvParameterSpec(new byte[IV_BYTES]);
+ c.init(Cipher.ENCRYPT_MODE, k, iv);
+ // Encrypt a blank plaintext
+ byte[] plaintext = new byte[123];
+ byte[] ciphertext = c.doFinal(plaintext);
+ // Erase the key and encrypt again - erase() was called after doFinal()
+ k.erase();
+ byte[] ciphertext1 = c.doFinal(plaintext);
+ // Encrypt again - this time erase() was called before doFinal()
+ byte[] ciphertext2 = c.doFinal(plaintext);
+ // The ciphertexts should match
+ assertArrayEquals(ciphertext, ciphertext1);
+ assertArrayEquals(ciphertext, ciphertext2);
+ }
+
+ @Test
+ public void testErasureDoesNotAffectMac() throws Exception {
+ byte[] key = new byte[KEY_BYTES];
+ random.nextBytes(key);
+ ErasableKey k = new ErasableKeyImpl(key, CIPHER);
+ Mac m = Mac.getInstance(MAC);
+ m.init(k);
+ // Authenticate a blank plaintext
+ byte[] plaintext = new byte[123];
+ byte[] mac = m.doFinal(plaintext);
+ // Erase the key and authenticate again
+ k.erase();
+ byte[] mac1 = m.doFinal(plaintext);
+ // Authenticate again
+ byte[] mac2 = m.doFinal(plaintext);
+ // The MACs should match
+ assertArrayEquals(mac, mac1);
+ assertArrayEquals(mac, mac2);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java b/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java
new file mode 100644
index 000000000..01a893940
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/crypto/KeyAgreementTest.java
@@ -0,0 +1,25 @@
+package net.sf.briar.crypto;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.security.KeyPair;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+
+import org.junit.Test;
+
+public class KeyAgreementTest extends BriarTestCase {
+
+ @Test
+ public void testKeyAgreement() throws Exception {
+ CryptoComponent crypto = new CryptoComponentImpl();
+ KeyPair a = crypto.generateAgreementKeyPair();
+ byte[] aPub = a.getPublic().getEncoded();
+ KeyPair b = crypto.generateAgreementKeyPair();
+ byte[] bPub = b.getPublic().getEncoded();
+ byte[] aSecret = crypto.deriveInitialSecret(aPub, b, true);
+ byte[] bSecret = crypto.deriveInitialSecret(bPub, a, false);
+ assertArrayEquals(aSecret, bSecret);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java b/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java
new file mode 100644
index 000000000..b05f536a4
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/crypto/KeyDerivationTest.java
@@ -0,0 +1,76 @@
+package net.sf.briar.crypto;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.crypto.ErasableKey;
+
+import org.junit.Test;
+
+public class KeyDerivationTest extends BriarTestCase {
+
+ private final CryptoComponent crypto;
+ private final byte[] secret;
+
+ public KeyDerivationTest() {
+ super();
+ crypto = new CryptoComponentImpl();
+ secret = new byte[32];
+ new Random().nextBytes(secret);
+ }
+
+ @Test
+ public void testKeysAreDistinct() {
+ List keys = new ArrayList();
+ keys.add(crypto.deriveFrameKey(secret, 0, false, false));
+ keys.add(crypto.deriveFrameKey(secret, 0, false, true));
+ keys.add(crypto.deriveFrameKey(secret, 0, true, false));
+ keys.add(crypto.deriveFrameKey(secret, 0, true, true));
+ keys.add(crypto.deriveTagKey(secret, true));
+ keys.add(crypto.deriveTagKey(secret, false));
+ for(int i = 0; i < 4; i++) {
+ byte[] keyI = keys.get(i).getEncoded();
+ for(int j = 0; j < 4; j++) {
+ byte[] keyJ = keys.get(j).getEncoded();
+ assertEquals(i == j, Arrays.equals(keyI, keyJ));
+ }
+ }
+ }
+
+ @Test
+ public void testSecretAffectsDerivation() {
+ Random r = new Random();
+ List secrets = new ArrayList();
+ for(int i = 0; i < 20; i++) {
+ byte[] b = new byte[32];
+ r.nextBytes(b);
+ secrets.add(crypto.deriveNextSecret(b, 0));
+ }
+ for(int i = 0; i < 20; i++) {
+ byte[] secretI = secrets.get(i);
+ for(int j = 0; j < 20; j++) {
+ byte[] secretJ = secrets.get(j);
+ assertEquals(i == j, Arrays.equals(secretI, secretJ));
+ }
+ }
+ }
+
+ @Test
+ public void testConnectionNumberAffectsDerivation() {
+ List secrets = new ArrayList();
+ for(int i = 0; i < 20; i++) {
+ secrets.add(crypto.deriveNextSecret(secret.clone(), i));
+ }
+ for(int i = 0; i < 20; i++) {
+ byte[] secretI = secrets.get(i);
+ for(int j = 0; j < 20; j++) {
+ byte[] secretJ = secrets.get(j);
+ assertEquals(i == j, Arrays.equals(secretI, secretJ));
+ }
+ }
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/db/BasicH2Test.java b/briar-tests/src/net/sf/briar/db/BasicH2Test.java
new file mode 100644
index 000000000..76e2384f6
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/db/BasicH2Test.java
@@ -0,0 +1,192 @@
+package net.sf.briar.db;
+
+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.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.TestUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BasicH2Test extends BriarTestCase {
+
+ private static final String CREATE_TABLE =
+ "CREATE TABLE foo"
+ + " (uniqueId BINARY(32),"
+ + " name VARCHAR NOT NULL)";
+
+ private final File testDir = TestUtils.getTestDirectory();
+ private final File db = new File(testDir, "db");
+ private final String url = "jdbc:h2:" + db.getPath();
+
+ private Connection connection = null;
+
+ @Before
+ public void setUp() throws Exception {
+ testDir.mkdirs();
+ Class.forName("org.h2.Driver");
+ connection = DriverManager.getConnection(url);
+ }
+
+ @Test
+ public void testCreateTableAndAddRow() throws Exception {
+ // Create the table
+ createTable(connection);
+ // Generate an ID
+ byte[] id = new byte[32];
+ new Random().nextBytes(id);
+ // Insert the ID and name into the table
+ addRow(id, "foo");
+ }
+
+ @Test
+ public void testCreateTableAddAndRetrieveRow() throws Exception {
+ // Create the table
+ createTable(connection);
+ // Generate an ID
+ byte[] id = new byte[32];
+ new Random().nextBytes(id);
+ // Insert the ID and name into the table
+ addRow(id, "foo");
+ // Check that the name can be retrieved using the ID
+ assertEquals("foo", getName(id));
+ }
+
+ @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, -128
+ };
+ 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, 0
+ };
+ 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, 127
+ };
+ // Create the table
+ createTable(connection);
+ // Insert the rows
+ addRow(first, "first");
+ addRow(second, "second");
+ addRow(third, "third");
+ addRow(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 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 addRow(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, Types.BINARY);
+ else ps.setBytes(1, id);
+ ps.setString(2, name);
+ int rowsAffected = ps.executeUpdate();
+ ps.close();
+ assertEquals(1, rowsAffected);
+ } catch(SQLException e) {
+ connection.close();
+ throw e;
+ }
+ }
+
+ private String getName(byte[] id) throws SQLException {
+ String sql = "SELECT name FROM foo WHERE uniqueID = ?";
+ try {
+ PreparedStatement ps = connection.prepareStatement(sql);
+ if(id != null) 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 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;
+ }
+ }
+
+ private List getNames() throws SQLException {
+ String sql = "SELECT name FROM foo ORDER BY uniqueId";
+ List 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;
+ }
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if(connection != null) connection.close();
+ TestUtils.deleteTestDirectory(testDir);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java b/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java
new file mode 100644
index 000000000..cbe77eda2
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/db/DatabaseCleanerImplTest.java
@@ -0,0 +1,67 @@
+package net.sf.briar.db;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import java.util.concurrent.CountDownLatch;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.clock.SystemTimer;
+import net.sf.briar.api.clock.Timer;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.db.DatabaseCleaner.Callback;
+
+import org.junit.Test;
+
+// FIXME: Use a mock timer
+public class DatabaseCleanerImplTest extends BriarTestCase {
+
+ @Test
+ public void testCleanerRunsPeriodically() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(5);
+ Callback callback = new Callback() {
+
+ public void checkFreeSpaceAndClean() throws DbException {
+ latch.countDown();
+ }
+
+ public boolean shouldCheckFreeSpace() {
+ return true;
+ }
+ };
+ Timer timer = new SystemTimer();
+ DatabaseCleanerImpl cleaner = new DatabaseCleanerImpl(timer);
+ // Start the cleaner
+ cleaner.startCleaning(callback, 10L);
+ // The database should be cleaned five times (allow 5s for system load)
+ assertTrue(latch.await(5, SECONDS));
+ // Stop the cleaner
+ cleaner.stopCleaning();
+ }
+
+ @Test
+ public void testStoppingCleanerWakesItUp() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ Callback callback = new Callback() {
+
+ public void checkFreeSpaceAndClean() throws DbException {
+ latch.countDown();
+ }
+
+ public boolean shouldCheckFreeSpace() {
+ return true;
+ }
+ };
+ Timer timer = new SystemTimer();
+ DatabaseCleanerImpl cleaner = new DatabaseCleanerImpl(timer);
+ long start = System.currentTimeMillis();
+ // Start the cleaner
+ cleaner.startCleaning(callback, 10L * 1000L);
+ // The database should be cleaned once at startup
+ assertTrue(latch.await(5, SECONDS));
+ // Stop the cleaner (it should be waiting between sweeps)
+ cleaner.stopCleaning();
+ long end = System.currentTimeMillis();
+ // Check that much less than 10 seconds expired
+ assertTrue(end - start < 10L * 1000L);
+ }
+}
diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java
new file mode 100644
index 000000000..389bb46a6
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentImplTest.java
@@ -0,0 +1,151 @@
+package net.sf.briar.db;
+
+import static net.sf.briar.db.DatabaseConstants.BYTES_PER_SWEEP;
+import static net.sf.briar.db.DatabaseConstants.MIN_FREE_SPACE;
+
+import java.util.Collections;
+
+import net.sf.briar.api.clock.SystemClock;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.lifecycle.ShutdownManager;
+import net.sf.briar.api.protocol.PacketFactory;
+import net.sf.briar.db.DatabaseCleaner.Callback;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.Test;
+
+/**
+ * Tests that use the DatabaseCleaner.Callback interface of
+ * DatabaseComponentImpl.
+ */
+public class DatabaseComponentImplTest extends DatabaseComponentTest {
+
+ @Test
+ public void testNotCleanedIfEnoughFreeSpace() throws DbException {
+ Mockery context = new Mockery();
+ @SuppressWarnings("unchecked")
+ final Database