diff --git a/briar-tests/.classpath b/briar-tests/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..5e92e5c69cbb1d8a9a268fc88ad0dfab83671e97 --- /dev/null +++ b/briar-tests/.classpath @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="src" path="src"/> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/> + <classpathentry kind="lib" path="libs/hamcrest-core-1.1.jar"/> + <classpathentry kind="lib" path="libs/hamcrest-library-1.1.jar"/> + <classpathentry kind="lib" path="libs/jmock-2.5.1.jar"/> + <classpathentry kind="lib" path="libs/junit-4.9b3.jar"/> + <classpathentry combineaccessrules="false" kind="src" path="/briar-core"/> + <classpathentry kind="lib" path="/briar-core/android.jar"/> + <classpathentry kind="output" path="bin"/> +</classpath> diff --git a/briar-tests/.gitignore b/briar-tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ba077a4031add5b3a04384f8b9cfc414efbf47dd --- /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 0000000000000000000000000000000000000000..fd15c6bba7eaa47b3b2c282d8ef8cf6fd534eecc --- /dev/null +++ b/briar-tests/.project @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>briar-tests</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> 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 0000000000000000000000000000000000000000..5f1d5ce0c3d60472692cda24885c92042a693ac0 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 0000000000000000000000000000000000000000..40610c9b4a2343cc8891a055dcd301e8d142d937 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 0000000000000000000000000000000000000000..4415dfbc94f8515bef4495976bcdb0fa5e6b5981 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 0000000000000000000000000000000000000000..8c784e5810eed4a350abcd9d48afe20b8f20eb2c 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 0000000000000000000000000000000000000000..94260a35069411445665db3f9b9b41ee347f32af --- /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 0000000000000000000000000000000000000000..f0bea95468f7f8d930b0a92b3746f159832c9115 --- /dev/null +++ b/briar-tests/src/build.xml @@ -0,0 +1,118 @@ +<project name='test' default='test'> + <fileset id='core-jars' dir='../../briar-core/libs'> + <include name='*.jar'/> + </fileset> + <fileset id='test-jars' dir='../libs'> + <include name='*.jar'/> + </fileset> + <path id='android-jar'> + <pathelement location='../../briar-core/android.jar'/> + </path> + <path id='core-classes'> + <pathelement location='../../briar-core/build'/> + </path> + <path id='test-classes'> + <pathelement location='../build'/> + </path> + <target name='clean'> + <delete dir='../../briar-core/build'/> + <delete dir='../build'/> + <delete dir='test.tmp'/> + </target> + <target name='compile'> + <mkdir dir='../../briar-core/build'/> + <javac srcdir='../../briar-core/src' + destdir='../../briar-core/build' source='1.5' + includeantruntime='false' debug='off'> + <classpath> + <fileset refid='core-jars'/> + <path refid='android-jar'/> + <path refid='core-classes'/> + </classpath> + </javac> + <mkdir dir='../build'/> + <javac srcdir='.' destdir='../build' source='1.5' + includeantruntime='false' debug='off'> + <classpath> + <fileset refid='core-jars'/> + <fileset refid='test-jars'/> + <path refid='android-jar'/> + <path refid='core-classes'/> + <path refid='test-classes'/> + </classpath> + </javac> + </target> + <target name='test' depends='compile'> + <junit printsummary='on' fork='yes' forkmode='once'> + <assertions> + <enable/> + </assertions> + <classpath> + <fileset refid='core-jars'/> + <fileset refid='test-jars'/> + <path refid='core-classes'/> + <path refid='test-classes'/> + </classpath> + <jvmarg value='-Djava.library.path=../../briar-core/libs'/> + <test name='net.sf.briar.LockFairnessTest'/> + <test name='net.sf.briar.ProtocolIntegrationTest'/> + <test name='net.sf.briar.crypto.CounterModeTest'/> + <test name='net.sf.briar.crypto.ErasableKeyTest'/> + <test name='net.sf.briar.crypto.KeyAgreementTest'/> + <test name='net.sf.briar.crypto.KeyDerivationTest'/> + <test name='net.sf.briar.db.BasicH2Test'/> + <test name='net.sf.briar.db.DatabaseCleanerImplTest'/> + <test name='net.sf.briar.db.DatabaseComponentImplTest'/> + <test name='net.sf.briar.lifecycle.ShutdownManagerImplTest'/> + <test name='net.sf.briar.lifecycle.WindowsShutdownManagerImplTest'/> + <test name='net.sf.briar.plugins.PluginManagerImplTest'/> + <test name='net.sf.briar.plugins.file.LinuxRemovableDriveFinderTest'/> + <test name='net.sf.briar.plugins.file.MacRemovableDriveFinderTest'/> + <test name='net.sf.briar.plugins.file.PollingRemovableDriveMonitorTest'/> + <test name='net.sf.briar.plugins.file.RemovableDrivePluginTest'/> + <test name='net.sf.briar.plugins.file.UnixRemovableDriveMonitorTest'/> + <test name='net.sf.briar.plugins.tcp.LanTcpPluginTest'/> + <test name='net.sf.briar.protocol.AckReaderTest'/> + <test name='net.sf.briar.protocol.BatchReaderTest'/> + <test name='net.sf.briar.protocol.ConstantsTest'/> + <test name='net.sf.briar.protocol.ConsumersTest'/> + <test name='net.sf.briar.protocol.OfferReaderTest'/> + <test name='net.sf.briar.protocol.ProtocolIntegrationTest'/> + <test name='net.sf.briar.protocol.ProtocolWriterImplTest'/> + <test name='net.sf.briar.protocol.RequestReaderTest'/> + <test name='net.sf.briar.protocol.UnverifiedBatchImplTest'/> + <test name='net.sf.briar.protocol.simplex.OutgoingSimplexConnectionTest'/> + <test name='net.sf.briar.protocol.simplex.SimplexProtocolIntegrationTest'/> + <test name='net.sf.briar.serial.ReaderImplTest'/> + <test name='net.sf.briar.serial.WriterImplTest'/> + <test name='net.sf.briar.transport.ConnectionReaderImplTest'/> + <test name='net.sf.briar.transport.ConnectionRegistryImplTest'/> + <test name='net.sf.briar.transport.ConnectionWindowTest'/> + <test name='net.sf.briar.transport.ConnectionWriterImplTest'/> + <test name='net.sf.briar.transport.IncomingEncryptionLayerTest'/> + <test name='net.sf.briar.transport.OutgoingEncryptionLayerTest'/> + <test name='net.sf.briar.transport.TransportIntegrationTest'/> + <test name='net.sf.briar.transport.TransportConnectionRecogniserTest'/> + <test name='net.sf.briar.util.ByteUtilsTest'/> + <test name='net.sf.briar.util.FileUtilsTest'/> + <test name='net.sf.briar.util.StringUtilsTest'/> + <test name='net.sf.briar.util.ZipUtilsTest'/> + </junit> + </target> + <target name='test-slow' depends='compile'> + <junit printsummary='withOutAndErr' fork='yes' forkmode='once'> + <assertions> + <enable/> + </assertions> + <classpath> + <fileset refid='core-jars'/> + <fileset refid='test-jars'/> + <path refid='core-classes'/> + <path refid='test-classes'/> + </classpath> + <jvmarg value='-Djava.library.path=../../briar-core/libs'/> + <test name='net.sf.briar.db.H2DatabaseTest'/> + <test name='net.sf.briar.plugins.tor.TorPluginTest'/> + </junit> + </target> +</project> 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 0000000000000000000000000000000000000000..32f496aef4ab891bc4d8de2f19b1deb320b36d8f --- /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 0000000000000000000000000000000000000000..5560855a0d6cbb2847472fcb66e29e392f8d93e3 --- /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 0000000000000000000000000000000000000000..bbd7af2f9a7edb4387cac173311376d32b2fd1af --- /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<Transport> 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<byte[]> batch = Arrays.asList(message.getSerialised(), + message1.getSerialised(), message2.getSerialised(), + message3.getSerialised()); + RawBatch b = packetFactory.createBatch(batch); + writer.writeBatch(b); + + Collection<MessageId> 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<Group, Long> subs = new LinkedHashMap<Group, Long>(); + subs.put(group, 0L); + subs.put(group1, 0L); + SubscriptionUpdate s = packetFactory.createSubscriptionUpdate( + Collections.<GroupId, GroupId>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<Message> messages = b.getMessages(); + assertEquals(4, messages.size()); + Iterator<Message> 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<MessageId> offered = o.getMessageIds(); + assertEquals(4, offered.size()); + Iterator<MessageId> 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<Group, Long> 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 0000000000000000000000000000000000000000..fdfedb413f245fa2bd9095d184a7ec0a125c9129 --- /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 0000000000000000000000000000000000000000..5479d9c6bd62248c3f605e59776b770a3d68bf1d --- /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 0000000000000000000000000000000000000000..e5c4f6cfe231e52535f32b75d12a551a250911ff --- /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 0000000000000000000000000000000000000000..96cde200146d6cbfd3ee34df429afa8b525e65b3 --- /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<Bytes> ciphertexts = new HashSet<Bytes>(); + 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<Bytes> ciphertexts = new HashSet<Bytes>(); + 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 0000000000000000000000000000000000000000..eb448a5505c74ff09741eb247fab91c77fb489a8 --- /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 0000000000000000000000000000000000000000..01a8939409f95afe8e3244eb452f54818a2ded0a --- /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 0000000000000000000000000000000000000000..b05f536a45430b94af0e81c42a79c68b2720c6a4 --- /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<ErasableKey> keys = new ArrayList<ErasableKey>(); + 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<byte[]> secrets = new ArrayList<byte[]>(); + 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<byte[]> secrets = new ArrayList<byte[]>(); + 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 0000000000000000000000000000000000000000..76e2384f663dd8e2d51f3f6512fb5e1c1f1956b9 --- /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<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 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<String> getNames() throws SQLException { + String sql = "SELECT name FROM foo ORDER BY uniqueId"; + List<String> names = new ArrayList<String>(); + 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 0000000000000000000000000000000000000000..cbe77eda24e55292e8ef8825d69703d01002ac34 --- /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 0000000000000000000000000000000000000000..389bb46a6314464aae821f5d7a8a32bad54c6fd0 --- /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<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testCleanedIfNotEnoughFreeSpace() throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.emptyList())); + oneOf(database).commitTransaction(txn); + // As if by magic, some free space has appeared + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testExpiringUnsendableMessageDoesNotTriggerBackwardInclusion() + throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).removeMessage(txn, messageId); + oneOf(database).commitTransaction(txn); + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Test + public void testExpiringSendableMessageTriggersBackwardInclusion() + throws DbException { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE - 1)); + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getOldMessages(txn, BYTES_PER_SWEEP); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(1)); + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).removeMessage(txn, messageId); + oneOf(database).commitTransaction(txn); + oneOf(database).getFreeSpace(); + will(returnValue(MIN_FREE_SPACE)); + }}); + Callback db = createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + + db.checkFreeSpaceAndClean(); + + context.assertIsSatisfied(); + } + + @Override + protected <T> DatabaseComponent createDatabaseComponent( + Database<T> database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory) { + return createDatabaseComponentImpl(database, cleaner, shutdown, + packetFactory); + } + + private <T> DatabaseComponentImpl<T> createDatabaseComponentImpl( + Database<T> database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory) { + return new DatabaseComponentImpl<T>(database, cleaner, shutdown, + packetFactory, new SystemClock()); + } +} diff --git a/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java new file mode 100644 index 0000000000000000000000000000000000000000..73a4545dfde556f8858c778506420c38130803ca --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/DatabaseComponentTest.java @@ -0,0 +1,1606 @@ +package net.sf.briar.db; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.NoSuchContactException; +import net.sf.briar.api.db.NoSuchContactTransportException; +import net.sf.briar.api.db.event.ContactAddedEvent; +import net.sf.briar.api.db.event.ContactRemovedEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessagesAddedEvent; +import net.sf.briar.api.db.event.RatingChangedEvent; +import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent; +import net.sf.briar.api.lifecycle.ShutdownManager; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.AuthorId; +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.GroupId; +import net.sf.briar.api.protocol.Message; +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.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.ContactTransport; +import net.sf.briar.api.transport.TemporarySecret; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public abstract class DatabaseComponentTest extends BriarTestCase { + + protected final Object txn = new Object(); + protected final AuthorId authorId; + protected final BatchId batchId; + protected final ContactId contactId; + protected final GroupId groupId; + protected final MessageId messageId, parentId; + private final String subject; + private final long timestamp; + private final int size; + private final byte[] raw; + private final Message message, privateMessage; + private final Group group; + private final TransportId transportId; + private final Collection<Transport> transports; + private final ContactTransport contactTransport; + private final TemporarySecret temporarySecret; + + public DatabaseComponentTest() { + super(); + authorId = new AuthorId(TestUtils.getRandomId()); + batchId = new BatchId(TestUtils.getRandomId()); + contactId = new ContactId(234); + groupId = new GroupId(TestUtils.getRandomId()); + messageId = new MessageId(TestUtils.getRandomId()); + parentId = new MessageId(TestUtils.getRandomId()); + subject = "Foo"; + timestamp = System.currentTimeMillis(); + size = 1234; + raw = new byte[size]; + message = new TestMessage(messageId, null, groupId, authorId, subject, + timestamp, raw); + privateMessage = new TestMessage(messageId, null, null, null, subject, + timestamp, raw); + group = new TestGroup(groupId, "The really exciting group", null); + transportId = new TransportId(TestUtils.getRandomId()); + TransportProperties properties = new TransportProperties( + Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + transports = Collections.singletonList(transport); + contactTransport = new ContactTransport(contactId, transportId, 123L, + 234L, 345L, true); + temporarySecret = new TemporarySecret(contactId, transportId, 1L, 2L, + 3L, false, 4L, new byte[32], 5L, 6L, new byte[4]); + } + + protected abstract <T> DatabaseComponent createDatabaseComponent( + Database<T> database, DatabaseCleaner cleaner, + ShutdownManager shutdown, PacketFactory packetFactory); + + @Test + @SuppressWarnings("unchecked") + public void testSimpleCalls() throws Exception { + final int shutdownHandle = 12345; + Mockery context = new Mockery(); + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Group group = context.mock(Group.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + // open(false) + oneOf(database).open(false); + oneOf(cleaner).startCleaning( + with(any(DatabaseCleaner.Callback.class)), + with(any(long.class))); + oneOf(shutdown).addShutdownHook(with(any(Runnable.class))); + will(returnValue(shutdownHandle)); + // getRating(authorId) + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + // setRating(authorId, Rating.GOOD) + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.emptyList())); + oneOf(listener).eventOccurred(with(any(RatingChangedEvent.class))); + // setRating(authorId, Rating.GOOD) again + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.GOOD)); + // addContact() + oneOf(database).addContact(txn); + will(returnValue(contactId)); + oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class))); + // getContacts() + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // getTransportProperties(transportId) + oneOf(database).getRemoteProperties(txn, transportId); + will(returnValue(Collections.emptyMap())); + // subscribe(group) + oneOf(group).getId(); + will(returnValue(groupId)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(false)); + oneOf(database).addSubscription(txn, group); + // subscribe(group) again + oneOf(group).getId(); + will(returnValue(groupId)); + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + // getMessageHeaders(groupId) + oneOf(database).getMessageHeaders(txn, groupId); + will(returnValue(Collections.emptyList())); + // getSubscriptions() + oneOf(database).getSubscriptions(txn); + will(returnValue(Collections.singletonList(groupId))); + // unsubscribe(groupId) + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(true)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(Collections.emptyList())); + oneOf(database).removeSubscription(txn, groupId); + // unsubscribe(groupId) again + oneOf(database).containsSubscription(txn, groupId); + will(returnValue(false)); + // removeContact(contactId) + oneOf(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(database).removeContact(txn, contactId); + oneOf(listener).eventOccurred(with(any(ContactRemovedEvent.class))); + // close() + oneOf(shutdown).removeShutdownHook(shutdownHandle); + oneOf(cleaner).stopCleaning(); + oneOf(database).close(); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.open(false); + db.addListener(listener); + assertEquals(Rating.UNRATED, db.getRating(authorId)); + db.setRating(authorId, Rating.GOOD); // First time - listeners called + db.setRating(authorId, Rating.GOOD); // Second time - not called + assertEquals(contactId, db.addContact()); + assertEquals(Collections.singletonList(contactId), db.getContacts()); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(transportId)); + db.subscribe(group); // First time - listeners called + db.subscribe(group); // Second time - not called + assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId)); + assertEquals(Collections.singletonList(groupId), db.getSubscriptions()); + db.unsubscribe(groupId); // First time - listeners called + db.unsubscribe(groupId); // Second time - not called + db.removeContact(contactId); + db.removeListener(listener); + db.close(); + + context.assertIsSatisfied(); + } + + @Test + public void testNullParentStopsBackwardInclusion() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + allowing(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // Backward inclusion stops when the message has no parent + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testUnaffectedParentStopsBackwardInclusion() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // The parent exists, is in the DB, and is in the same group + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(parentId)); + // The parent is already sendable + oneOf(database).getSendability(txn, parentId); + will(returnValue(1)); + oneOf(database).setSendability(txn, parentId, 2); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testAffectedParentContinuesBackwardInclusion() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // setRating(authorId, Rating.GOOD) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).setRating(txn, authorId, Rating.GOOD); + will(returnValue(Rating.UNRATED)); + // The sendability of the author's messages should be incremented + oneOf(database).getMessagesByAuthor(txn, authorId); + will(returnValue(Collections.singletonList(messageId))); + oneOf(database).getSendability(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 1); + // The parent exists, is in the DB, and is in the same group + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(parentId)); + // The parent is not already sendable + oneOf(database).getSendability(txn, parentId); + will(returnValue(0)); + oneOf(database).setSendability(txn, parentId, 1); + // The parent has no parent + oneOf(database).getGroupMessageParent(txn, parentId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setRating(authorId, Rating.GOOD); + + context.assertIsSatisfied(); + } + + @Test + public void testGroupMessagesAreNotStoredUnlessSubscribed() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testDuplicateGroupMessagesAreNotStored() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddLocalGroupMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The author is unrated and there are no sendable children + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingSendableMessageTriggersBackwardInclusion() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The author is rated GOOD and there are two sendable children + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.GOOD)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(2)); + oneOf(database).setSendability(txn, messageId, 3); + // The sendability of the message's ancestors should be updated + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testDuplicatePrivateMessagesAreNotStored() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testAddLocalPrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testVariousMethodsThrowExceptionIfContactIsMissing() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + final Batch batch = context.mock(Batch.class); + final Offer offer = context.mock(Offer.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + // Check whether the contact is in the DB (which it's not) + exactly(16).of(database).startTransaction(); + will(returnValue(txn)); + exactly(16).of(database).containsContact(txn, contactId); + will(returnValue(false)); + exactly(16).of(database).abortTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + try { + db.addContactTransport(contactTransport); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.addLocalPrivateMessage(privateMessage, contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateAck(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateBatch(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateBatch(contactId, 123, + Collections.<MessageId>emptyList()); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateOffer(contactId, 123); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateSubscriptionUpdate(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.generateTransportUpdate(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.hasSendableMessages(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveAck(contactId, ack); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveBatch(contactId, batch); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveOffer(contactId, offer); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveSubscriptionUpdate(contactId, subscriptionUpdate); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.receiveTransportUpdate(contactId, transportUpdate); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.removeContact(contactId); + fail(); + } catch(NoSuchContactException expected) {} + + try { + db.setSeen(contactId, Collections.singletonList(messageId)); + fail(); + } catch(NoSuchContactException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testVariousMethodsThrowExceptionIfContactTransportIsMissing() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // Check whether the contact transport is in the DB (which it's not) + exactly(2).of(database).startTransaction(); + will(returnValue(txn)); + exactly(2).of(database).containsContactTransport(txn, contactId, + transportId); + will(returnValue(false)); + exactly(2).of(database).abortTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + try { + db.incrementConnectionCounter(contactId, transportId, 0L); + fail(); + } catch(NoSuchContactTransportException expected) {} + + try { + db.setConnectionWindow(contactId, transportId, 0L, 0L, new byte[4]); + fail(); + } catch(NoSuchContactTransportException expected) {} + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateAck() throws Exception { + final BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + final Collection<BatchId> batchesToAck = new ArrayList<BatchId>(); + batchesToAck.add(batchId); + batchesToAck.add(batchId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the batches to ack + oneOf(database).getBatchesToAck(txn, contactId, 123); + will(returnValue(batchesToAck)); + // Create the packet + oneOf(packetFactory).createAck(batchesToAck); + will(returnValue(ack)); + // Record the batches that were acked + oneOf(database).removeBatchesToAck(txn, contactId, batchesToAck); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(ack, db.generateAck(contactId, 123)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateBatch() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final byte[] raw1 = new byte[size]; + final Collection<MessageId> sendable = Arrays.asList(messageId, + messageId1); + final Collection<byte[]> messages = Arrays.asList(raw, raw1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final RawBatch batch = context.mock(RawBatch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the sendable messages + oneOf(database).getSendableMessages(txn, contactId, size * 2); + will(returnValue(sendable)); + oneOf(database).getMessage(txn, messageId); + will(returnValue(raw)); + oneOf(database).getMessage(txn, messageId1); + will(returnValue(raw1)); + // Create the packet + oneOf(packetFactory).createBatch(messages); + will(returnValue(batch)); + // Record the outstanding batch + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addOutstandingBatch(txn, contactId, batchId, + sendable); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(batch, db.generateBatch(contactId, size * 2)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateBatchFromRequest() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + final byte[] raw1 = new byte[size]; + final Collection<MessageId> requested = new ArrayList<MessageId>(); + requested.add(messageId); + requested.add(messageId1); + requested.add(messageId2); + final Collection<byte[]> msgs = Arrays.asList(raw1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final RawBatch batch = context.mock(RawBatch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Try to get the requested messages + oneOf(database).getMessageIfSendable(txn, contactId, messageId); + will(returnValue(null)); // Message is not sendable + oneOf(database).getMessageIfSendable(txn, contactId, messageId1); + will(returnValue(raw1)); // Message is sendable + oneOf(database).getMessageIfSendable(txn, contactId, messageId2); + will(returnValue(null)); // Message is not sendable + // Create the packet + oneOf(packetFactory).createBatch(msgs); + will(returnValue(batch)); + // Record the outstanding batch + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId1)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(batch, db.generateBatch(contactId, size * 3, requested)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateOffer() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final Collection<MessageId> offerable = new ArrayList<MessageId>(); + offerable.add(messageId); + offerable.add(messageId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Offer offer = context.mock(Offer.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the sendable message IDs + oneOf(database).getOfferableMessages(txn, contactId, 123); + will(returnValue(offerable)); + // Create the packet + oneOf(packetFactory).createOffer(offerable); + will(returnValue(offer)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(offer, db.generateOffer(contactId, 123)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateSubscriptionUpdate() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the visible holes and subscriptions + oneOf(database).getVisibleHoles(with(txn), with(contactId), + with(any(long.class))); + will(returnValue(Collections.emptyMap())); + oneOf(database).getVisibleSubscriptions(with(txn), with(contactId), + with(any(long.class))); + will(returnValue(Collections.singletonMap(group, 0L))); + // Get the expiry time + oneOf(database).getExpiryTime(txn); + will(returnValue(0L)); + // Create the packet + oneOf(packetFactory).createSubscriptionUpdate( + with(Collections.<GroupId, GroupId>emptyMap()), + with(Collections.singletonMap(group, 0L)), + with(any(long.class)), + with(any(long.class))); + will(returnValue(subscriptionUpdate)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(subscriptionUpdate, + db.generateSubscriptionUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportUpdateNotSentUnlessDue() throws Exception { + final long now = System.currentTimeMillis(); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Check whether an update is due + oneOf(database).getTransportsModified(txn); + will(returnValue(now - 1L)); + oneOf(database).getTransportsSent(txn, contactId); + will(returnValue(now)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertNull(db.generateTransportUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testGenerateTransportUpdate() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Check whether an update is due + oneOf(database).getTransportsModified(txn); + will(returnValue(0L)); + oneOf(database).getTransportsSent(txn, contactId); + will(returnValue(0L)); + // Get the local transport properties + oneOf(database).getLocalTransports(txn); + will(returnValue(transports)); + oneOf(database).setTransportsSent(with(txn), with(contactId), + with(any(long.class))); + // Create the packet + oneOf(packetFactory).createTransportUpdate(with(transports), + with(any(long.class))); + will(returnValue(transportUpdate)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(transportUpdate, db.generateTransportUpdate(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveAck() throws Exception { + final BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the acked batches + oneOf(ack).getBatchIds(); + will(returnValue(Collections.singletonList(batchId))); + oneOf(database).removeAckedBatch(txn, contactId, batchId); + // Find lost batches + oneOf(database).getLostBatches(txn, contactId); + will(returnValue(Collections.singletonList(batchId1))); + oneOf(database).removeLostBatch(txn, contactId, batchId1); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveAck(contactId, ack); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchStoresPrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(privateMessage))); + // The message is stored + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // The batch must be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchWithDuplicatePrivateMessage() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(privateMessage))); + // The message is stored, but it's a duplicate + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + // The batch must still be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchDoesNotStoreGroupMessageUnlessSubscribed() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(false)); + // The message is not stored but the batch must still be acked + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchDoesNotCalculateSendabilityForDuplicates() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, but it's a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchCalculatesSendability() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, and it's not a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // Set the status to NEW for all other contacts (there are none) + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // Calculate the sendability - zero, so ancestors aren't updated + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveBatchUpdatesAncestorSendability() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Batch batch = context.mock(Batch.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Only store messages belonging to visible, subscribed groups + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + oneOf(database).containsVisibleSubscription(txn, groupId, + contactId, timestamp); + will(returnValue(true)); + // The message is stored, and it's not a duplicate + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.SEEN); + // Set the status to NEW for all other contacts (there are none) + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + // Calculate the sendability - ancestors are updated + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.GOOD)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(1)); + oneOf(database).setSendability(txn, messageId, 2); + oneOf(database).getGroupMessageParent(txn, messageId); + will(returnValue(null)); + // The batch needs to be acknowledged + oneOf(batch).getId(); + will(returnValue(batchId)); + oneOf(database).addBatchToAck(txn, contactId, batchId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveBatch(contactId, batch); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveOffer() throws Exception { + final MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + final MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + final Collection<MessageId> offered = new ArrayList<MessageId>(); + offered.add(messageId); + offered.add(messageId1); + offered.add(messageId2); + final BitSet expectedRequest = new BitSet(3); + expectedRequest.set(0); + expectedRequest.set(2); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final Offer offer = context.mock(Offer.class); + final Request request = context.mock(Request.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the offered messages + oneOf(offer).getMessageIds(); + will(returnValue(offered)); + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + will(returnValue(false)); // Not visible - request message # 0 + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId1); + will(returnValue(true)); // Visible - do not request message # 1 + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId2); + will(returnValue(false)); // Not visible - request message # 2 + // Create the packet + oneOf(packetFactory).createRequest(expectedRequest, 3); + will(returnValue(request)); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + assertEquals(request, db.receiveOffer(contactId, offer)); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveSubscriptionUpdate() throws Exception { + final GroupId start = new GroupId(TestUtils.getRandomId()); + final GroupId end = new GroupId(TestUtils.getRandomId()); + final long expiry = 1234L, timestamp = 5678L; + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final SubscriptionUpdate subscriptionUpdate = + context.mock(SubscriptionUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the contents of the update + oneOf(subscriptionUpdate).getHoles(); + will(returnValue(Collections.singletonMap(start, end))); + oneOf(subscriptionUpdate).getSubscriptions(); + will(returnValue(Collections.singletonMap(group, 0L))); + oneOf(subscriptionUpdate).getExpiryTime(); + will(returnValue(expiry)); + oneOf(subscriptionUpdate).getTimestamp(); + will(returnValue(timestamp)); + // Store the contents of the update + oneOf(database).removeSubscriptions(txn, contactId, start, end); + oneOf(database).addSubscription(txn, contactId, group, 0L); + oneOf(database).setExpiryTime(txn, contactId, expiry); + oneOf(database).setSubscriptionsReceived(txn, contactId, timestamp); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveSubscriptionUpdate(contactId, subscriptionUpdate); + + context.assertIsSatisfied(); + } + + @Test + public void testReceiveTransportUpdate() throws Exception { + final long timestamp = 1234L; + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final TransportUpdate transportUpdate = + context.mock(TransportUpdate.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // Get the contents of the update + oneOf(transportUpdate).getTransports(); + will(returnValue(transports)); + oneOf(transportUpdate).getTimestamp(); + will(returnValue(timestamp)); + oneOf(database).setTransports(txn, contactId, transports, + timestamp); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.receiveTransportUpdate(contactId, transportUpdate); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingGroupMessageCallsListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(true)); + oneOf(database).getContacts(txn); + will(returnValue(Collections.singletonList(contactId))); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + oneOf(database).getRating(txn, authorId); + will(returnValue(Rating.UNRATED)); + oneOf(database).getNumberOfSendableChildren(txn, messageId); + will(returnValue(0)); + oneOf(database).setSendability(txn, messageId, 0); + oneOf(database).commitTransaction(txn); + // The message was added, so the listener should be called + oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingPrivateMessageCallsListeners() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(true)); + oneOf(database).setStatus(txn, contactId, messageId, Status.NEW); + // The message was added, so the listener should be called + oneOf(listener).eventOccurred(with(any(MessagesAddedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingDuplicateGroupMessageDoesNotCallListeners() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + // addLocalGroupMessage(message) + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsSubscription(txn, groupId, timestamp); + will(returnValue(true)); + oneOf(database).addGroupMessage(txn, message); + will(returnValue(false)); + oneOf(database).commitTransaction(txn); + // The message was not added, so the listener should not be called + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalGroupMessage(message); + + context.assertIsSatisfied(); + } + + @Test + public void testAddingDuplicatePrivateMessageDoesNotCallListeners() + throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // addLocalPrivateMessage(privateMessage, contactId) + oneOf(database).addPrivateMessage(txn, privateMessage, contactId); + will(returnValue(false)); + // The message was not added, so the listener should not be called + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.addLocalPrivateMessage(privateMessage, contactId); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportPropertiesChangedCallsListeners() + throws Exception { + final TransportProperties properties = + new TransportProperties(Collections.singletonMap("bar", "baz")); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getLocalProperties(txn, transportId); + will(returnValue(new TransportProperties())); + oneOf(database).mergeLocalProperties(txn, transportId, properties); + oneOf(database).setTransportsModified(with(txn), + with(any(long.class))); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.mergeLocalProperties(transportId, properties); + + context.assertIsSatisfied(); + } + + @Test + public void testTransportPropertiesUnchangedDoesNotCallListeners() + throws Exception { + final TransportProperties properties = + new TransportProperties(Collections.singletonMap("bar", "baz")); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getLocalProperties(txn, transportId); + will(returnValue(properties)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.mergeLocalProperties(transportId, properties); + + context.assertIsSatisfied(); + } + + @Test + public void testSetSeen() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + allowing(database).startTransaction(); + will(returnValue(txn)); + allowing(database).commitTransaction(txn); + allowing(database).containsContact(txn, contactId); + will(returnValue(true)); + // setSeen(contactId, Collections.singletonList(messageId)) + oneOf(database).setStatusSeenIfVisible(txn, contactId, messageId); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.setSeen(contactId, Collections.singletonList(messageId)); + + context.assertIsSatisfied(); + } + + @Test + public void testVisibilityChangedCallsListeners() throws Exception { + final ContactId contactId1 = new ContactId(123); + final Collection<ContactId> both = Arrays.asList(contactId, contactId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(both)); + oneOf(database).getContacts(txn); + will(returnValue(both)); + oneOf(database).removeVisibility(txn, contactId1, groupId); + oneOf(database).commitTransaction(txn); + oneOf(listener).eventOccurred(with(any( + SubscriptionsUpdatedEvent.class))); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.setVisibility(groupId, Collections.singletonList(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testVisibilityUnchangedDoesNotCallListeners() throws Exception { + final ContactId contactId1 = new ContactId(234); + final Collection<ContactId> both = Arrays.asList(contactId, contactId1); + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + final DatabaseListener listener = context.mock(DatabaseListener.class); + context.checking(new Expectations() {{ + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getVisibility(txn, groupId); + will(returnValue(both)); + oneOf(database).getContacts(txn); + will(returnValue(both)); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addListener(listener); + db.setVisibility(groupId, both); + + context.assertIsSatisfied(); + } + + @Test + public void testTemporarySecrets() throws Exception { + Mockery context = new Mockery(); + @SuppressWarnings("unchecked") + final Database<Object> database = context.mock(Database.class); + final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class); + final ShutdownManager shutdown = context.mock(ShutdownManager.class); + final PacketFactory packetFactory = context.mock(PacketFactory.class); + context.checking(new Expectations() {{ + // addSecrets() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).containsContactTransport(txn, contactId, + transportId); + will(returnValue(true)); + oneOf(database).addSecrets(txn, + Collections.singletonList(temporarySecret)); + oneOf(database).commitTransaction(txn); + // getSecrets() + oneOf(database).startTransaction(); + will(returnValue(txn)); + oneOf(database).getSecrets(txn); + will(returnValue(Collections.singletonList(temporarySecret))); + oneOf(database).commitTransaction(txn); + }}); + DatabaseComponent db = createDatabaseComponent(database, cleaner, + shutdown, packetFactory); + + db.addSecrets(Collections.singletonList(temporarySecret)); + assertEquals(Collections.singletonList(temporarySecret), + db.getSecrets()); + + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java new file mode 100644 index 0000000000000000000000000000000000000000..842caa434eded9133a4434b48470bf7c494c9d5e --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/H2DatabaseTest.java @@ -0,0 +1,2044 @@ +package net.sf.briar.db; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.sf.briar.db.DatabaseConstants.RETRANSMIT_THRESHOLD; +import static org.junit.Assert.assertArrayEquals; + +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.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestDatabaseConfig; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.Rating; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.MessageHeader; +import net.sf.briar.api.protocol.AuthorId; +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.MessageId; +import net.sf.briar.api.protocol.Transport; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ContactTransport; +import net.sf.briar.api.transport.TemporarySecret; + +import org.apache.commons.io.FileSystemUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class H2DatabaseTest extends BriarTestCase { + + 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 Random random = new Random(); + private final GroupFactory groupFactory; + private final Group group; + private final AuthorId authorId; + private final BatchId batchId; + private final ContactId contactId; + private final GroupId groupId; + private final MessageId messageId, privateMessageId; + private final String subject; + private final long timestamp; + private final int size; + private final byte[] raw; + private final Message message, privateMessage; + private final TransportId transportId; + + public H2DatabaseTest() throws Exception { + super(); + groupFactory = new TestGroupFactory(); + authorId = new AuthorId(TestUtils.getRandomId()); + batchId = new BatchId(TestUtils.getRandomId()); + contactId = new ContactId(1); + groupId = new GroupId(TestUtils.getRandomId()); + messageId = new MessageId(TestUtils.getRandomId()); + privateMessageId = new MessageId(TestUtils.getRandomId()); + group = new TestGroup(groupId, "Foo", null); + subject = "Foo"; + timestamp = System.currentTimeMillis(); + size = 1234; + raw = new byte[size]; + random.nextBytes(raw); + message = new TestMessage(messageId, null, groupId, authorId, subject, + timestamp, raw); + privateMessage = new TestMessage(privateMessageId, null, null, null, + subject, timestamp, raw); + transportId = new TransportId(TestUtils.getRandomId()); + } + + @Before + public void setUp() { + 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)); + assertEquals(contactId, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId)); + assertFalse(db.containsSubscription(txn, groupId)); + db.addSubscription(txn, group); + assertTrue(db.containsSubscription(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + db.addGroupMessage(txn, message); + assertTrue(db.containsMessage(txn, messageId)); + assertFalse(db.containsMessage(txn, privateMessageId)); + db.addPrivateMessage(txn, privateMessage, contactId); + assertTrue(db.containsMessage(txn, privateMessageId)); + 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.containsSubscription(txn, groupId)); + assertTrue(db.containsMessage(txn, messageId)); + byte[] raw1 = db.getMessage(txn, messageId); + assertArrayEquals(raw, raw1); + assertTrue(db.containsMessage(txn, privateMessageId)); + raw1 = db.getMessage(txn, privateMessageId); + assertArrayEquals(raw, raw1); + // Delete the records + db.removeMessage(txn, messageId); + db.removeMessage(txn, privateMessageId); + db.removeContact(txn, contactId); + db.removeSubscription(txn, groupId); + db.commitTransaction(txn); + db.close(); + + // Check that the records are gone + db = open(true); + txn = db.startTransaction(); + assertFalse(db.containsContact(txn, contactId)); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(txn, transportId)); + assertFalse(db.containsSubscription(txn, groupId)); + assertFalse(db.containsMessage(txn, messageId)); + assertFalse(db.containsMessage(txn, privateMessageId)); + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContactIdsIncrease() throws Exception { + ContactId contactId1 = new ContactId(2); + ContactId contactId2 = new ContactId(3); + ContactId contactId3 = new ContactId(4); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Create three contacts + assertFalse(db.containsContact(txn, contactId)); + assertEquals(contactId, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId)); + assertFalse(db.containsContact(txn, contactId1)); + assertEquals(contactId1, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId1)); + assertFalse(db.containsContact(txn, contactId2)); + assertEquals(contactId2, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId2)); + // Delete the contact with the highest ID + db.removeContact(txn, contactId2); + assertFalse(db.containsContact(txn, contactId2)); + // Add another contact - a new ID should be created + assertFalse(db.containsContact(txn, contactId3)); + assertEquals(contactId3, db.addContact(txn)); + assertTrue(db.containsContact(txn, contactId3)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRatings() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Unknown authors should be unrated + assertEquals(Rating.UNRATED, db.getRating(txn, authorId)); + // Store a rating + db.setRating(txn, authorId, Rating.GOOD); + // Check that the rating was stored + assertEquals(Rating.GOOD, db.getRating(txn, authorId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testUnsubscribingRemovesGroupMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Unsubscribing from the group should remove the message + assertTrue(db.containsMessage(txn, messageId)); + db.removeSubscription(txn, groupId); + assertFalse(db.containsMessage(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemovingContactRemovesPrivateMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + + // Removing the contact should remove the message + assertTrue(db.containsMessage(txn, privateMessageId)); + db.removeContact(txn, contactId); + assertFalse(db.containsMessage(txn, privateMessageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendablePrivateMessagesMustHaveStatusNew() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + + // The message has no status yet, so it should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to NEW should make the message sendable + db.setStatus(txn, contactId, privateMessageId, Status.NEW); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(privateMessageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the status to SENT should make the message unsendable + db.setStatus(txn, contactId, privateMessageId, Status.SENT); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to SEEN should also make the message unsendable + db.setStatus(txn, contactId, privateMessageId, Status.SEEN); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendablePrivateMessagesMustFitCapacity() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and store a private message + assertEquals(contactId, db.addContact(txn)); + db.addPrivateMessage(txn, privateMessage, contactId); + db.setStatus(txn, contactId, privateMessageId, Status.NEW); + + // The message is sendable, but too large to send + assertTrue(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, size - 1).iterator(); + assertFalse(it.hasNext()); + + // The message is just the right size to send + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(privateMessageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustHavePositiveSendability() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the sendability to > 0 should make the message sendable + db.setSendability(txn, messageId, 1); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the sendability to 0 should make the message unsendable + db.setSendability(txn, messageId, 0); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustHaveStatusNew() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + + // The message has no status yet, so it should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to Status.NEW should make the message sendable + db.setStatus(txn, contactId, messageId, Status.NEW); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Changing the status to SENT should make the message unsendable + db.setStatus(txn, contactId, messageId, Status.SENT); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the status to SEEN should also make the message unsendable + db.setStatus(txn, contactId, messageId, Status.SEEN); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeSubscribed() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The contact is not subscribed, so the message should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // The contact subscribing should make the message sendable + db.addSubscription(txn, contactId, group, 0L); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // The contact unsubscribing should make the message unsendable + db.removeSubscriptions(txn, contactId, null, null); + assertFalse(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeNewerThanSubscriptions() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is older than the contact's subscription, so it should + // not be sendable + db.addSubscription(txn, contactId, group, timestamp + 1); + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Changing the contact's subscription should make the message sendable + db.removeSubscriptions(txn, contactId, null, null); + db.addSubscription(txn, contactId, group, timestamp); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustFitCapacity() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is sendable, but too large to send + assertTrue(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, size - 1).iterator(); + assertFalse(it.hasNext()); + + // The message is just the right size to send + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSendableGroupMessagesMustBeVisible() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible to the contact, so the message + // should not be sendable + assertFalse(db.hasSendableMessages(txn, contactId)); + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Making the subscription visible should make the message sendable + db.addVisibility(txn, contactId, groupId); + assertTrue(db.hasSendableMessages(txn, contactId)); + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testBatchesToAck() throws Exception { + BatchId batchId1 = new BatchId(TestUtils.getRandomId()); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and some batches to ack + assertEquals(contactId, db.addContact(txn)); + db.addBatchToAck(txn, contactId, batchId); + db.addBatchToAck(txn, contactId, batchId1); + + // Both batch IDs should be returned + Collection<BatchId> acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(2, acks.size()); + assertTrue(acks.contains(batchId)); + assertTrue(acks.contains(batchId1)); + + // Remove the batch IDs + db.removeBatchesToAck(txn, contactId, acks); + + // Both batch IDs should have been removed + acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(0, acks.size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testDuplicateBatchesReceived() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and receive the same batch twice + assertEquals(contactId, db.addContact(txn)); + db.addBatchToAck(txn, contactId, batchId); + db.addBatchToAck(txn, contactId, batchId); + + // The batch ID should only be returned once + Collection<BatchId> acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(1, acks.size()); + assertTrue(acks.contains(batchId)); + + // Remove the batch ID + db.removeBatchesToAck(txn, contactId, acks); + + // The batch ID should have been removed + acks = db.getBatchesToAck(txn, contactId, 1234); + assertEquals(0, acks.size()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSameBatchCannotBeSentTwice() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Add an outstanding batch + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // It should not be possible to add the same outstanding batch again + try { + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + fail(); + } catch(DbException expected) {} + + db.abortTransaction(txn); + db.close(); + } + + @Test + public void testSameBatchCanBeSentToDifferentContacts() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add two contacts, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + ContactId contactId1 = db.addContact(txn); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // Add an outstanding batch for the first contact + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // Add the same outstanding batch for the second contact + db.addOutstandingBatch(txn, contactId1, batchId, + Collections.singletonList(messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemoveAckedBatch() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // Retrieve the message from the database and mark it as sent + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + db.setStatus(txn, contactId, messageId, Status.SENT); + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // The message should no longer be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Pretend that the batch was acked + db.removeAckedBatch(txn, contactId, batchId); + + // The message still should not be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRemoveLostBatch() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // Get the message and mark it as sent + Iterator<MessageId> it = + db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + db.setStatus(txn, contactId, messageId, Status.SENT); + db.addOutstandingBatch(txn, contactId, batchId, + Collections.singletonList(messageId)); + + // The message should no longer be sendable + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertFalse(it.hasNext()); + + // Pretend that the batch was lost + db.removeLostBatch(txn, contactId, batchId); + + // The message should be sendable again + it = db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testRetransmission() throws Exception { + BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD + 5]; + for(int i = 0; i < ids.length; i++) { + ids[i] = new BatchId(TestUtils.getRandomId()); + } + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + assertEquals(contactId, db.addContact(txn)); + + // Add some outstanding batches, a few ms apart + for(int i = 0; i < ids.length; i++) { + db.addOutstandingBatch(txn, contactId, ids[i], + Collections.<MessageId>emptyList()); + Thread.sleep(5); + } + + // The contact acks the batches in reverse order. The first + // RETRANSMIT_THRESHOLD - 1 acks should not trigger any retransmissions + for(int i = 0; i < RETRANSMIT_THRESHOLD - 1; i++) { + db.removeAckedBatch(txn, contactId, ids[ids.length - i - 1]); + Collection<BatchId> lost = db.getLostBatches(txn, contactId); + assertEquals(Collections.emptyList(), lost); + } + + // The next ack should trigger the retransmission of the remaining + // five outstanding batches + int index = ids.length - RETRANSMIT_THRESHOLD; + db.removeAckedBatch(txn, contactId, ids[index]); + Collection<BatchId> lost = db.getLostBatches(txn, contactId); + for(int i = 0; i < index; i++) { + assertTrue(lost.contains(ids[i])); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testNoRetransmission() throws Exception { + BatchId[] ids = new BatchId[RETRANSMIT_THRESHOLD * 2]; + for(int i = 0; i < ids.length; i++) { + ids[i] = new BatchId(TestUtils.getRandomId()); + } + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact + assertEquals(contactId, db.addContact(txn)); + + // Add some outstanding batches, a few ms apart + for(int i = 0; i < ids.length; i++) { + db.addOutstandingBatch(txn, contactId, ids[i], + Collections.<MessageId>emptyList()); + Thread.sleep(5); + } + + // The contact acks the batches in the order they were sent - nothing + // should be retransmitted + for(int i = 0; i < ids.length; i++) { + db.removeAckedBatch(txn, contactId, ids[i]); + Collection<BatchId> lost = db.getLostBatches(txn, contactId); + assertEquals(Collections.emptyList(), lost); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessagesByAuthor() throws Exception { + AuthorId authorId1 = new AuthorId(TestUtils.getRandomId()); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, authorId1, + subject, timestamp, raw); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store two messages + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message1); + + // Check that each message is retrievable via its author + Iterator<MessageId> it = + db.getMessagesByAuthor(txn, authorId).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + it = db.getMessagesByAuthor(txn, authorId1).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId1, it.next()); + assertFalse(it.hasNext()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetNumberOfSendableChildren() throws Exception { + MessageId childId1 = new MessageId(TestUtils.getRandomId()); + MessageId childId2 = new MessageId(TestUtils.getRandomId()); + MessageId childId3 = new MessageId(TestUtils.getRandomId()); + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Another group name", + null); + Message child1 = new TestMessage(childId1, messageId, groupId, + authorId, subject, timestamp, raw); + Message child2 = new TestMessage(childId2, messageId, groupId, + authorId, subject, timestamp, raw); + // The third child is in a different group + Message child3 = new TestMessage(childId3, messageId, groupId1, + authorId, subject, timestamp, raw); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to the groups and store the messages + db.addSubscription(txn, group); + db.addSubscription(txn, group1); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, child1); + db.addGroupMessage(txn, child2); + db.addGroupMessage(txn, child3); + // Make all the children sendable + db.setSendability(txn, childId1, 1); + db.setSendability(txn, childId2, 5); + db.setSendability(txn, childId3, 3); + + // There should be two sendable children + assertEquals(2, db.getNumberOfSendableChildren(txn, messageId)); + // Make one of the children unsendable + db.setSendability(txn, childId1, 0); + // Now there should be one sendable child + assertEquals(1, db.getNumberOfSendableChildren(txn, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetOldMessages() throws Exception { + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, authorId, + subject, timestamp + 1000, raw); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store two messages + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addGroupMessage(txn, message1); + + // Allowing enough capacity for one message should return the older one + Iterator<MessageId> it = db.getOldMessages(txn, size).iterator(); + assertTrue(it.hasNext()); + assertEquals(messageId, it.next()); + assertFalse(it.hasNext()); + + // Allowing enough capacity for both messages should return both + Collection<MessageId> ids = new HashSet<MessageId>(); + for(MessageId id : db.getOldMessages(txn, size * 2)) ids.add(id); + assertEquals(2, ids.size()); + assertTrue(ids.contains(messageId)); + assertTrue(ids.contains(messageId1)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetFreeSpace() throws Exception { + byte[] largeBody = new byte[ONE_MEGABYTE]; + for(int i = 0; i < largeBody.length; i++) largeBody[i] = (byte) i; + Message message1 = new TestMessage(messageId, null, groupId, authorId, + subject, timestamp, largeBody); + Database<Connection> db = open(false); + + // Sanity check: there should be enough space on disk for this test + String path = testDir.getAbsolutePath(); + assertTrue(FileSystemUtils.freeSpaceKb(path) * 1024L > 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.addSubscription(txn, group); + db.addGroupMessage(txn, message1); + db.commitTransaction(txn); + assertTrue(db.getFreeSpace() < free); + + db.close(); + } + + @Test + public void testCloseWaitsForCommit() throws Exception { + final CountDownLatch closing = new CountDownLatch(1); + final CountDownLatch closed = new CountDownLatch(1); + final AtomicBoolean transactionFinished = new AtomicBoolean(false); + final AtomicBoolean error = new AtomicBoolean(false); + final Database<Connection> db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread() { + public void run() { + 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 { + final CountDownLatch closing = new CountDownLatch(1); + final CountDownLatch closed = new CountDownLatch(1); + final AtomicBoolean transactionFinished = new AtomicBoolean(false); + final AtomicBoolean error = new AtomicBoolean(false); + final Database<Connection> db = open(false); + + // Start a transaction + Connection txn = db.startTransaction(); + // In another thread, close the database + Thread close = new Thread() { + public void run() { + 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 testUpdateTransportProperties() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a transport + TransportProperties properties = + new TransportProperties(Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + assertEquals(contactId, db.addContact(txn)); + db.setTransports(txn, contactId, + Collections.singletonList(transport), 1); + assertEquals(Collections.singletonMap(contactId, properties), + db.getRemoteProperties(txn, transportId)); + + // Replace the transport properties + TransportProperties properties1 = + new TransportProperties(Collections.singletonMap("baz", "bam")); + Transport transport1 = new Transport(transportId, properties1); + db.setTransports(txn, contactId, + Collections.singletonList(transport1), 2); + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + // Remove the transport properties + TransportProperties properties2 = new TransportProperties(); + Transport transport2 = new Transport(transportId, properties2); + db.setTransports(txn, contactId, + Collections.singletonList(transport2), 3); + assertEquals(Collections.emptyMap(), + db.getRemoteProperties(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testLocalTransports() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Set the transport properties + TransportProperties properties = new TransportProperties(); + properties.put("foo", "foo"); + properties.put("bar", "bar"); + db.mergeLocalProperties(txn, transportId, properties); + assertEquals(Collections.singletonList(properties), + db.getLocalTransports(txn)); + + // Update one of the properties and add another + TransportProperties properties1 = new TransportProperties(); + properties1.put("bar", "baz"); + properties1.put("bam", "bam"); + db.mergeLocalProperties(txn, transportId, properties1); + TransportProperties expected = new TransportProperties(); + expected.put("foo", "foo"); + expected.put("bar", "baz"); + expected.put("bam", "bam"); + assertEquals(Collections.singletonList(expected), + db.getLocalTransports(txn)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testUpdateTransportConfig() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Set the transport config + TransportConfig config = new TransportConfig(); + config.put("foo", "foo"); + config.put("bar", "bar"); + db.mergeConfig(txn, transportId, config); + assertEquals(config, db.getConfig(txn, transportId)); + + // Update one of the properties and add another + TransportConfig config1 = new TransportConfig(); + config1.put("bar", "baz"); + config1.put("bam", "bam"); + db.mergeConfig(txn, transportId, config1); + TransportConfig expected = new TransportConfig(); + expected.put("foo", "foo"); + expected.put("bar", "baz"); + expected.put("bam", "bam"); + assertEquals(expected, db.getConfig(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testTransportsNotUpdatedIfTimestampIsOld() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a transport + TransportProperties properties = + new TransportProperties(Collections.singletonMap("foo", "bar")); + Transport transport = new Transport(transportId, properties); + assertEquals(contactId, db.addContact(txn)); + db.setTransports(txn, contactId, + Collections.singletonList(transport), 1); + assertEquals(Collections.singletonMap(contactId, properties), + db.getRemoteProperties(txn, transportId)); + + // Replace the transport properties using a timestamp of 2 + TransportProperties properties1 = + new TransportProperties(Collections.singletonMap("baz", "bam")); + Transport transport1 = new Transport(transportId, properties1); + db.setTransports(txn, contactId, + Collections.singletonList(transport1), 2); + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + // Try to replace the transport properties using a timestamp of 1 + TransportProperties properties2 = + new TransportProperties(Collections.singletonMap("quux", "etc")); + Transport transport2 = new Transport(transportId, properties2); + db.setTransports(txn, contactId, + Collections.singletonList(transport2), 1); + + // The old properties should still be there + assertEquals(Collections.singletonMap(contactId, properties1), + db.getRemoteProperties(txn, transportId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfNotInDatabase() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + + // The message is not in the database + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfSeen() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to SEEN + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.SEEN); + + // The message is not sendable because its status is SEEN + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfNotSendable() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to 0 and the status to NEW + db.setSendability(txn, messageId, 0); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is not sendable because its sendability is 0 + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsNullIfOld() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message - + // the message is older than the contact's subscription + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, timestamp + 1); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to NEW + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is not sendable because it's too old + assertNull(db.getMessageIfSendable(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageIfSendableReturnsMessage() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // Set the sendability to > 0 and the status to NEW + db.setSendability(txn, messageId, 1); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The message is sendable so it should be returned + byte[] b = db.getMessageIfSendable(txn, contactId, messageId); + assertArrayEquals(raw, b); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresMessageInDatabase() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + + // The message is not in the database + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresLocalSubscription() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact with a subscription + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, contactId, group, 0L); + + // There's no local subscription for the group + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresContactSubscription() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // There's no contact subscription for the group + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleRequiresVisibility() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + db.addSubscription(txn, contactId, group, 0L); + db.setStatus(txn, contactId, messageId, Status.NEW); + + // The subscription is not visible + assertFalse(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleReturnsTrueIfAlreadySeen() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // The message has already been seen by the contact + db.setStatus(txn, contactId, messageId, Status.SEEN); + + assertTrue(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetStatusSeenIfVisibleReturnsTrueIfNew() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact, subscribe to a group and store a message + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + db.addVisibility(txn, contactId, groupId); + db.addSubscription(txn, contactId, group, 0L); + db.addGroupMessage(txn, message); + + // The message has not been seen by the contact + db.setStatus(txn, contactId, messageId, Status.NEW); + + assertTrue(db.setStatusSeenIfVisible(txn, contactId, messageId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testVisibility() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + // The group should not be visible to the contact + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + // Make the group visible to the contact + db.addVisibility(txn, contactId, groupId); + assertEquals(Collections.singletonList(contactId), + db.getVisibility(txn, groupId)); + // Make the group invisible again + db.removeVisibility(txn, contactId, groupId); + assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithNoParent() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with no parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, null, groupId, null, subject, + timestamp, raw); + db.addGroupMessage(txn, child); + assertTrue(db.containsMessage(txn, childId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithAbsentParent() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with an absent parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + assertTrue(db.containsMessage(txn, childId)); + assertFalse(db.containsMessage(txn, parentId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithParentInAnotherGroup() + throws Exception { + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Group name", null); + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to two groups + db.addSubscription(txn, group); + db.addSubscription(txn, group1); + + // A message with a parent in another group should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + Message parent = new TestMessage(parentId, null, groupId1, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addGroupMessage(txn, parent); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, parentId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithPrivateParent() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + + // A message with a private parent should return null + MessageId childId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, privateMessageId, groupId, + null, subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addPrivateMessage(txn, privateMessage, contactId); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, privateMessageId)); + assertNull(db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetGroupMessageParentWithParentInSameGroup() + throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // A message with a parent in the same group should return the parent + MessageId childId = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + Message child = new TestMessage(childId, parentId, groupId, null, + subject, timestamp, raw); + Message parent = new TestMessage(parentId, null, groupId, null, + subject, timestamp, raw); + db.addGroupMessage(txn, child); + db.addGroupMessage(txn, parent); + assertTrue(db.containsMessage(txn, childId)); + assertTrue(db.containsMessage(txn, parentId)); + assertEquals(parentId, db.getGroupMessageParent(txn, childId)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageBody() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add a contact and subscribe to a group + assertEquals(contactId, db.addContact(txn)); + db.addSubscription(txn, group); + + // Store a couple of messages + int bodyLength = raw.length - 20; + Message message1 = new TestMessage(messageId, null, groupId, null, + subject, timestamp, raw, 5, bodyLength); + Message privateMessage1 = new TestMessage(privateMessageId, null, null, + null, subject, timestamp, raw, 10, bodyLength); + db.addGroupMessage(txn, message1); + db.addPrivateMessage(txn, privateMessage1, contactId); + + // Calculate the expected message bodies + byte[] expectedBody = new byte[bodyLength]; + System.arraycopy(raw, 5, expectedBody, 0, bodyLength); + assertFalse(Arrays.equals(expectedBody, new byte[bodyLength])); + byte[] expectedBody1 = new byte[bodyLength]; + System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); + System.arraycopy(raw, 10, expectedBody1, 0, bodyLength); + + // Retrieve the raw messages + assertArrayEquals(raw, db.getMessage(txn, messageId)); + assertArrayEquals(raw, db.getMessage(txn, privateMessageId)); + + // Retrieve the message bodies + byte[] body = db.getMessageBody(txn, messageId); + assertArrayEquals(expectedBody, body); + byte[] body1 = db.getMessageBody(txn, privateMessageId); + assertArrayEquals(expectedBody1, body1); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetMessageHeaders() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group + db.addSubscription(txn, group); + + // Store a couple of messages + db.addGroupMessage(txn, message); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + MessageId parentId = new MessageId(TestUtils.getRandomId()); + long timestamp1 = System.currentTimeMillis(); + Message message1 = new TestMessage(messageId1, parentId, groupId, + authorId, subject, timestamp1, raw); + db.addGroupMessage(txn, message1); + // Mark one of the messages read + assertFalse(db.setRead(txn, messageId, true)); + + // Retrieve the message headers + Collection<MessageHeader> headers = db.getMessageHeaders(txn, groupId); + Iterator<MessageHeader> it = headers.iterator(); + boolean messageFound = false, message1Found = false; + // First header (order is undefined) + assertTrue(it.hasNext()); + MessageHeader header = it.next(); + if(messageId.equals(header.getId())) { + assertHeadersMatch(message, header); + assertTrue(header.getRead()); + assertFalse(header.getStarred()); + messageFound = true; + } else if(messageId1.equals(header.getId())) { + assertHeadersMatch(message1, header); + assertFalse(header.getRead()); + assertFalse(header.getStarred()); + message1Found = true; + } else { + fail(); + } + // Second header + assertTrue(it.hasNext()); + header = it.next(); + if(messageId.equals(header.getId())) { + assertHeadersMatch(message, header); + assertTrue(header.getRead()); + assertFalse(header.getStarred()); + messageFound = true; + } else if(messageId1.equals(header.getId())) { + assertHeadersMatch(message1, header); + assertFalse(header.getRead()); + assertFalse(header.getStarred()); + message1Found = true; + } else { + fail(); + } + // No more headers + assertFalse(it.hasNext()); + assertTrue(messageFound); + assertTrue(message1Found); + + db.commitTransaction(txn); + db.close(); + } + + private void assertHeadersMatch(Message m, MessageHeader h) { + assertEquals(m.getId(), h.getId()); + if(m.getParent() == null) assertNull(h.getParent()); + else assertEquals(m.getParent(), h.getParent()); + if(m.getGroup() == null) assertNull(h.getGroup()); + else assertEquals(m.getGroup(), h.getGroup()); + if(m.getAuthor() == null) assertNull(h.getAuthor()); + else assertEquals(m.getAuthor(), h.getAuthor()); + assertEquals(m.getSubject(), h.getSubject()); + assertEquals(m.getTimestamp(), h.getTimestamp()); + } + + @Test + public void testReadFlag() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unread by default + assertFalse(db.getRead(txn, messageId)); + // Marking the message read should return the old value + assertFalse(db.setRead(txn, messageId, true)); + assertTrue(db.setRead(txn, messageId, true)); + // The message should be read + assertTrue(db.getRead(txn, messageId)); + // Marking the message unread should return the old value + assertTrue(db.setRead(txn, messageId, false)); + assertFalse(db.setRead(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testStarredFlag() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a group and store a message + db.addSubscription(txn, group); + db.addGroupMessage(txn, message); + + // The message should be unstarred by default + assertFalse(db.getStarred(txn, messageId)); + // Starring the message should return the old value + assertFalse(db.setStarred(txn, messageId, true)); + assertTrue(db.setStarred(txn, messageId, true)); + // The message should be starred + assertTrue(db.getStarred(txn, messageId)); + // Unstarring the message should return the old value + assertTrue(db.setStarred(txn, messageId, false)); + assertFalse(db.setStarred(txn, messageId, false)); + // Unsubscribe from the group + db.removeSubscription(txn, groupId); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testGetUnreadMessageCounts() throws Exception { + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to a couple of groups + db.addSubscription(txn, group); + GroupId groupId1 = new GroupId(TestUtils.getRandomId()); + Group group1 = groupFactory.createGroup(groupId1, "Another group", + null); + db.addSubscription(txn, group1); + + // Store two messages in the first group + db.addGroupMessage(txn, message); + MessageId messageId1 = new MessageId(TestUtils.getRandomId()); + Message message1 = new TestMessage(messageId1, null, groupId, + authorId, subject, timestamp, raw); + db.addGroupMessage(txn, message1); + + // Store one message in the second group + MessageId messageId2 = new MessageId(TestUtils.getRandomId()); + Message message2 = new TestMessage(messageId2, null, groupId1, + authorId, subject, timestamp, raw); + db.addGroupMessage(txn, message2); + + // Mark one of the messages in the first group read + assertFalse(db.setRead(txn, messageId, true)); + + // There should be one unread message in each group + Map<GroupId, Integer> counts = db.getUnreadMessageCounts(txn); + assertEquals(2, counts.size()); + Integer count = counts.get(groupId); + assertNotNull(count); + assertEquals(1, count.intValue()); + count = counts.get(groupId1); + assertNotNull(count); + assertEquals(1, count.intValue()); + + // Mark the read message unread (it will now be false rather than null) + assertTrue(db.setRead(txn, messageId, false)); + + // Mark the message in the second group read + assertFalse(db.setRead(txn, messageId2, true)); + + // There should be two unread messages in the first group, none in + // the second group + counts = db.getUnreadMessageCounts(txn); + assertEquals(1, counts.size()); + count = counts.get(groupId); + assertNotNull(count); + assertEquals(2, count.intValue()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testMultipleSubscriptionsAndUnsubscriptions() throws Exception { + // Create some groups + List<Group> groups = new ArrayList<Group>(); + for(int i = 0; i < 100; i++) { + GroupId id = new GroupId(TestUtils.getRandomId()); + groups.add(groupFactory.createGroup(id, "Group name", null)); + } + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Subscribe to the groups and add a contact + for(Group g : groups) db.addSubscription(txn, g); + assertEquals(contactId, db.addContact(txn)); + + // Make the groups visible to the contact + Collections.shuffle(groups); + for(Group g : groups) db.addVisibility(txn, contactId, g.getId()); + + // Make some of the groups invisible to the contact and remove them all + Collections.shuffle(groups); + for(Group g : groups) { + if(Math.random() < 0.5) + db.removeVisibility(txn, contactId, g.getId()); + db.removeSubscription(txn, g.getId()); + } + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testTemporarySecrets() throws Exception { + // Create a contact transport and three consecutive temporary secrets + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long outgoing1 = 456L, centre1 = 567L; + long outgoing2 = 678L, centre2 = 789L; + long outgoing3 = 890L, centre3 = 901L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret1 = new byte[32], bitmap1 = new byte[4]; + random.nextBytes(secret1); + random.nextBytes(bitmap1); + TemporarySecret s1 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 0L, secret1, outgoing1, centre1, + bitmap1); + byte[] secret2 = new byte[32], bitmap2 = new byte[4]; + random.nextBytes(secret2); + random.nextBytes(bitmap2); + TemporarySecret s2 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 1L, secret2, outgoing2, centre2, + bitmap2); + byte[] secret3 = new byte[32], bitmap3 = new byte[4]; + random.nextBytes(secret3); + random.nextBytes(bitmap3); + TemporarySecret s3 = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, 2L, secret3, outgoing3, centre3, + bitmap3); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Initially there should be no secrets in the database + assertEquals(Collections.emptyList(), db.getSecrets(txn)); + + // Add the contact transport and the first two secrets + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s1, s2)); + + // Retrieve the first two secrets + Collection<TemporarySecret> secrets = db.getSecrets(txn); + assertEquals(2, secrets.size()); + boolean foundFirst = false, foundSecond = false; + for(TemporarySecret s : secrets) { + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(epoch, s.getEpoch()); + assertEquals(clockDiff, s.getClockDifference()); + assertEquals(latency, s.getLatency()); + assertEquals(alice, s.getAlice()); + if(s.getPeriod() == 0L) { + assertArrayEquals(secret1, s.getSecret()); + assertEquals(outgoing1, s.getOutgoingConnectionCounter()); + assertEquals(centre1, s.getWindowCentre()); + assertArrayEquals(bitmap1, s.getWindowBitmap()); + foundFirst = true; + } else if(s.getPeriod() == 1L) { + assertArrayEquals(secret2, s.getSecret()); + assertEquals(outgoing2, s.getOutgoingConnectionCounter()); + assertEquals(centre2, s.getWindowCentre()); + assertArrayEquals(bitmap2, s.getWindowBitmap()); + foundSecond = true; + } else { + fail(); + } + } + assertTrue(foundFirst); + assertTrue(foundSecond); + + // Adding the third secret (period 2) should delete the first (period 0) + db.addSecrets(txn, Arrays.asList(s3)); + secrets = db.getSecrets(txn); + assertEquals(2, secrets.size()); + foundSecond = false; + boolean foundThird = false; + for(TemporarySecret s : secrets) { + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(epoch, s.getEpoch()); + assertEquals(clockDiff, s.getClockDifference()); + assertEquals(latency, s.getLatency()); + assertEquals(alice, s.getAlice()); + if(s.getPeriod() == 1L) { + assertArrayEquals(secret2, s.getSecret()); + assertEquals(outgoing2, s.getOutgoingConnectionCounter()); + assertEquals(centre2, s.getWindowCentre()); + assertArrayEquals(bitmap2, s.getWindowBitmap()); + foundSecond = true; + } else if(s.getPeriod() == 2L) { + assertArrayEquals(secret3, s.getSecret()); + assertEquals(outgoing3, s.getOutgoingConnectionCounter()); + assertEquals(centre3, s.getWindowCentre()); + assertArrayEquals(bitmap3, s.getWindowBitmap()); + foundThird = true; + } else { + fail(); + } + } + assertTrue(foundSecond); + assertTrue(foundThird); + + // Removing the contact should remove the secrets + db.removeContact(txn, contactId); + assertEquals(Collections.emptyList(), db.getSecrets(txn)); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testIncrementConnectionCounter() throws Exception { + // Create a contact transport and a temporary secret + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long period = 456L, outgoing = 567L, centre = 678L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret = new byte[32], bitmap = new byte[4]; + random.nextBytes(secret); + TemporarySecret s = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, period, secret, outgoing, centre, + bitmap); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact transport and the temporary secret + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s)); + + // Retrieve the secret + Collection<TemporarySecret> secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Increment the connection counter twice and retrieve the secret again + assertEquals(outgoing, db.incrementConnectionCounter(txn, + s.getContactId(), s.getTransportId(), s.getPeriod())); + assertEquals(outgoing + 1L, db.incrementConnectionCounter(txn, + s.getContactId(), s.getTransportId(), s.getPeriod())); + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing + 2L, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testSetConnectionWindow() throws Exception { + // Create a contact transport and a temporary secret + long epoch = 123L, clockDiff = 234L, latency = 345L; + boolean alice = false; + long period = 456L, outgoing = 567L, centre = 678L; + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, clockDiff, latency, alice); + Random random = new Random(); + byte[] secret = new byte[32], bitmap = new byte[4]; + random.nextBytes(secret); + TemporarySecret s = new TemporarySecret(contactId, transportId, epoch, + clockDiff, latency, alice, period, secret, outgoing, centre, + bitmap); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Add the contact transport and the temporary secret + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct); + db.addSecrets(txn, Arrays.asList(s)); + + // Retrieve the secret + Collection<TemporarySecret> secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Update the connection window and retrieve the secret again + random.nextBytes(bitmap); + db.setConnectionWindow(txn, contactId, transportId, period, centre, + bitmap); + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + // Updating a nonexistent window should not throw an exception + db.setConnectionWindow(txn, contactId, transportId, period + 1L, 1L, + bitmap); + // The nonexistent window should not have been created + secrets = db.getSecrets(txn); + assertEquals(1, secrets.size()); + s = secrets.iterator().next(); + assertEquals(contactId, s.getContactId()); + assertEquals(transportId, s.getTransportId()); + assertEquals(period, s.getPeriod()); + assertArrayEquals(secret, s.getSecret()); + assertEquals(outgoing, s.getOutgoingConnectionCounter()); + assertEquals(centre, s.getWindowCentre()); + assertArrayEquals(bitmap, s.getWindowBitmap()); + + db.commitTransaction(txn); + db.close(); + } + + @Test + public void testContactTransports() throws Exception { + // Create some contact transports + long epoch1 = 123L, clockDiff1 = 234L, latency1 = 345L; + long epoch2 = 456L, clockDiff2 = 567L, latency2 = 678L; + boolean alice1 = true, alice2 = false; + TransportId transportId1 = new TransportId(TestUtils.getRandomId()); + TransportId transportId2 = new TransportId(TestUtils.getRandomId()); + ContactTransport ct1 = new ContactTransport(contactId, transportId1, + epoch1, clockDiff1, latency1, alice1); + ContactTransport ct2 = new ContactTransport(contactId, transportId2, + epoch2, clockDiff2, latency2, alice2); + + Database<Connection> db = open(false); + Connection txn = db.startTransaction(); + + // Initially there should be no contact transports in the database + assertEquals(Collections.emptyList(), db.getContactTransports(txn)); + + // Add a contact and the contact transports + assertEquals(contactId, db.addContact(txn)); + db.addContactTransport(txn, ct1); + db.addContactTransport(txn, ct2); + + // Retrieve the contact transports + Collection<ContactTransport> cts = db.getContactTransports(txn); + assertEquals(2, cts.size()); + boolean foundFirst = false, foundSecond = false; + for(ContactTransport ct : cts) { + assertEquals(contactId, ct.getContactId()); + if(ct.getTransportId().equals(transportId1)) { + assertEquals(epoch1, ct.getEpoch()); + assertEquals(clockDiff1, ct.getClockDifference()); + assertEquals(latency1, ct.getLatency()); + assertEquals(alice1, ct.getAlice()); + foundFirst = true; + } else if(ct.getTransportId().equals(transportId2)) { + assertEquals(epoch2, ct.getEpoch()); + assertEquals(clockDiff2, ct.getClockDifference()); + assertEquals(latency2, ct.getLatency()); + assertEquals(alice2, ct.getAlice()); + foundSecond = true; + } else { + fail(); + } + } + assertTrue(foundFirst); + assertTrue(foundSecond); + + // Removing the contact should remove the contact transports + db.removeContact(txn, contactId); + assertEquals(Collections.emptyList(), db.getContactTransports(txn)); + + 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.getMessage(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), groupFactory, + new SystemClock()); + db.open(resume); + return db; + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/db/TestGroup.java b/briar-tests/src/net/sf/briar/db/TestGroup.java new file mode 100644 index 0000000000000000000000000000000000000000..4e78e0788036179499b1ce2472ae1c8af8a7679e --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestGroup.java @@ -0,0 +1,29 @@ +package net.sf.briar.db; + +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupId; + +class TestGroup implements Group { + + private final GroupId id; + private final String name; + private final byte[] publicKey; + + public TestGroup(GroupId id, String name, byte[] publicKey) { + this.id = id; + this.name = name; + this.publicKey = publicKey; + } + + public GroupId getId() { + return id; + } + + public String getName() { + return name; + } + + public byte[] getPublicKey() { + return publicKey; + } +} diff --git a/briar-tests/src/net/sf/briar/db/TestGroupFactory.java b/briar-tests/src/net/sf/briar/db/TestGroupFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..89f29b92d14c217bdfad67c5f60ee3d399971313 --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestGroupFactory.java @@ -0,0 +1,20 @@ +package net.sf.briar.db; + +import java.io.IOException; + +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +import net.sf.briar.api.protocol.GroupId; + +class TestGroupFactory implements GroupFactory { + + public Group createGroup(String name, byte[] publicKey) throws IOException { + GroupId id = new GroupId(TestUtils.getRandomId()); + return new TestGroup(id, name, publicKey); + } + + public Group createGroup(GroupId id, String name, byte[] publicKey) { + return new TestGroup(id, name, publicKey); + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/db/TestMessage.java b/briar-tests/src/net/sf/briar/db/TestMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..4dbe7584a5b6296beafa3ccc47d8352ed6e869ea --- /dev/null +++ b/briar-tests/src/net/sf/briar/db/TestMessage.java @@ -0,0 +1,89 @@ +package net.sf.briar.db; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import net.sf.briar.api.protocol.AuthorId; +import net.sf.briar.api.protocol.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; + +class TestMessage implements Message { + + private final MessageId id, parent; + private final GroupId group; + private final AuthorId author; + private final String subject; + private final long timestamp; + private final byte[] raw; + private final int bodyStart, bodyLength; + + public TestMessage(MessageId id, MessageId parent, GroupId group, + AuthorId author, String subject, long timestamp, byte[] raw) { + this(id, parent, group, author, subject, timestamp, raw, 0, raw.length); + } + + public TestMessage(MessageId id, MessageId parent, GroupId group, + AuthorId author, String subject, long timestamp, byte[] raw, + int bodyStart, int bodyLength) { + this.id = id; + this.parent = parent; + this.group = group; + this.author = author; + this.subject = subject; + this.timestamp = timestamp; + this.raw = raw; + this.bodyStart = bodyStart; + this.bodyLength = bodyLength; + } + + public MessageId getId() { + return id; + } + + public MessageId getParent() { + return parent; + } + + public GroupId getGroup() { + return group; + } + + public AuthorId getAuthor() { + return author; + } + + public String getSubject() { + return subject; + } + + public long getTimestamp() { + return timestamp; + } + + public byte[] getSerialised() { + return raw; + } + + public int getBodyStart() { + return bodyStart; + } + + public int getBodyLength() { + return bodyLength; + } + + public InputStream getSerialisedStream() { + return new ByteArrayInputStream(raw); + } + + @Override + public boolean equals(Object o) { + return o instanceof Message && id.equals(((Message)o).getId()); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java b/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..da38215693134803e6a1dd0e008f65a91756f938 --- /dev/null +++ b/briar-tests/src/net/sf/briar/lifecycle/ShutdownManagerImplTest.java @@ -0,0 +1,33 @@ +package net.sf.briar.lifecycle; + +import java.util.HashSet; +import java.util.Set; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.lifecycle.ShutdownManager; + +import org.junit.Test; + +public class ShutdownManagerImplTest extends BriarTestCase { + + @Test + public void testAddAndRemove() { + ShutdownManager s = createShutdownManager(); + Set<Integer> handles = new HashSet<Integer>(); + for(int i = 0; i < 100; i++) { + int handle = s.addShutdownHook(new Runnable() { + public void run() {} + }); + // The handles should all be distinct + assertTrue(handles.add(handle)); + } + // The hooks should be removable + for(int handle : handles) assertTrue(s.removeShutdownHook(handle)); + // The hooks should no longer be removable + for(int handle : handles) assertFalse(s.removeShutdownHook(handle)); + } + + protected ShutdownManager createShutdownManager() { + return new ShutdownManagerImpl(); + } +} diff --git a/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java b/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ce872e97dec7983b0b8af3bd8ca1a076f5ac0c4b --- /dev/null +++ b/briar-tests/src/net/sf/briar/lifecycle/WindowsShutdownManagerImplTest.java @@ -0,0 +1,39 @@ +package net.sf.briar.lifecycle; + +import net.sf.briar.api.lifecycle.ShutdownManager; + +import org.junit.Test; + +public class WindowsShutdownManagerImplTest extends ShutdownManagerImplTest { + + @Override + protected ShutdownManager createShutdownManager() { + return new WindowsShutdownManagerImpl(); + } + + @Test + public void testManagerWaitsForHooksToRun() { + WindowsShutdownManagerImpl s = new WindowsShutdownManagerImpl(); + SlowHook[] hooks = new SlowHook[10]; + for(int i = 0; i < hooks.length; i++) { + hooks[i] = new SlowHook(); + s.addShutdownHook(hooks[i]); + } + s.runShutdownHooks(); + for(int i = 0; i < hooks.length; i++) assertTrue(hooks[i].finished); + } + + private static class SlowHook implements Runnable { + + private volatile boolean finished = false; + + public void run() { + try { + Thread.sleep(100); + finished = true; + } catch(InterruptedException e) { + fail(); + } + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2ffdace5eb168170e8cd7d40c6cf330071dc523d --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexClientTest.java @@ -0,0 +1,101 @@ +package net.sf.briar.plugins; + +import java.io.IOException; +import java.util.Map; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +public abstract class DuplexClientTest extends DuplexTest { + + protected ClientCallback callback = null; + + protected void run() throws IOException { + assert plugin != null; + // Start the plugin + System.out.println("Starting plugin"); + plugin.start(); + // Try to connect to the server + System.out.println("Creating connection"); + DuplexTransportConnection d = plugin.createConnection(contactId); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Try to send an invitation + System.out.println("Sending invitation"); + d = plugin.sendInvitation(getPseudoRandom(123), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Try to accept an invitation + System.out.println("Accepting invitation"); + d = plugin.acceptInvitation(getPseudoRandom(456), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + sendChallengeReceiveResponse(d); + } + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + } + + protected static class ClientCallback implements DuplexPluginCallback { + + private TransportConfig config = null; + private TransportProperties local = null; + private Map<ContactId, TransportProperties> remote = null; + + public ClientCallback(TransportConfig config, TransportProperties local, + Map<ContactId, TransportProperties> remote) { + this.config = config; + this.local = local; + this.remote = remote; + } + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map<ContactId, TransportProperties> getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config = c; + } + + public void mergeLocalProperties(TransportProperties p) { + local = p; + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) {} + + public void outgoingConnectionCreated(ContactId contactId, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..956eea2d33e09f207d9a57db16ad082a42f0db8a --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexServerTest.java @@ -0,0 +1,103 @@ +package net.sf.briar.plugins; + +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +public abstract class DuplexServerTest extends DuplexTest { + + protected ServerCallback callback = null; + + protected void run() throws Exception { + assert callback != null; + assert plugin != null; + // Start the plugin + System.out.println("Starting plugin"); + plugin.start(); + // Wait for a connection + System.out.println("Waiting for connection"); + callback.latch.await(); + // Try to accept an invitation + System.out.println("Accepting invitation"); + DuplexTransportConnection d = plugin.acceptInvitation( + getPseudoRandom(123), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + sendChallengeReceiveResponse(d); + } + // Try to send an invitation + System.out.println("Sending invitation"); + d = plugin.sendInvitation(getPseudoRandom(456), INVITATION_TIMEOUT); + if(d == null) { + System.out.println("Connection failed"); + } else { + System.out.println("Connection created"); + receiveChallengeSendResponse(d); + } + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + } + + protected class ServerCallback implements DuplexPluginCallback { + + private final CountDownLatch latch = new CountDownLatch(1); + + private TransportConfig config; + private TransportProperties local; + private Map<ContactId, TransportProperties> remote; + + public ServerCallback(TransportConfig config, TransportProperties local, + Map<ContactId, TransportProperties> remote) { + this.config = config; + this.local = local; + this.remote = remote; + } + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map<ContactId, TransportProperties> getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config = c; + } + + public void mergeLocalProperties(TransportProperties p) { + local = p; + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + System.out.println("Connection received"); + sendChallengeReceiveResponse(d); + latch.countDown(); + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/DuplexTest.java b/briar-tests/src/net/sf/briar/plugins/DuplexTest.java new file mode 100644 index 0000000000000000000000000000000000000000..80eee278f0c05acd481197c8fde43e62c57e04de --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/DuplexTest.java @@ -0,0 +1,98 @@ +package net.sf.briar.plugins; + +import java.io.IOException; +import java.io.PrintStream; +import java.util.Random; +import java.util.Scanner; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.PseudoRandom; +import net.sf.briar.api.plugins.duplex.DuplexPlugin; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +abstract class DuplexTest { + + protected static final String CHALLENGE = "Carrots!"; + protected static final String RESPONSE = "Potatoes!"; + protected static final long INVITATION_TIMEOUT = 30 * 1000; + + protected final ContactId contactId = new ContactId(234); + + protected DuplexPlugin plugin = null; + + protected void sendChallengeReceiveResponse(DuplexTransportConnection d) { + assert plugin != null; + try { + PrintStream out = new PrintStream(d.getOutputStream()); + out.println(CHALLENGE); + System.out.println("Sent challenge: " + CHALLENGE); + Scanner in = new Scanner(d.getInputStream()); + if(in.hasNextLine()) { + String response = in.nextLine(); + System.out.println("Received response: " + response); + if(RESPONSE.equals(response)) { + System.out.println("Correct response"); + } else { + System.out.println("Incorrect response"); + } + } else { + System.out.println("No response"); + } + d.dispose(false, true); + } catch(IOException e) { + e.printStackTrace(); + try { + d.dispose(true, true); + } catch(IOException e1) { + e1.printStackTrace(); + } + } + } + + protected void receiveChallengeSendResponse(DuplexTransportConnection d) { + assert plugin != null; + try { + Scanner in = new Scanner(d.getInputStream()); + if(in.hasNextLine()) { + String challenge = in.nextLine(); + System.out.println("Received challenge: " + challenge); + if(CHALLENGE.equals(challenge)) { + PrintStream out = new PrintStream(d.getOutputStream()); + out.println(RESPONSE); + System.out.println("Sent response: " + RESPONSE); + } else { + System.out.println("Incorrect challenge"); + } + } else { + System.out.println("No challenge"); + } + d.dispose(false, true); + } catch(IOException e) { + e.printStackTrace(); + try { + d.dispose(true, true); + } catch(IOException e1) { + e1.printStackTrace(); + } + } + } + + protected PseudoRandom getPseudoRandom(int seed) { + return new TestPseudoRandom(seed); + } + + private static class TestPseudoRandom implements PseudoRandom { + + private final Random r; + + private TestPseudoRandom(int seed) { + r = new Random(seed); + } + + public byte[] nextBytes(int bytes) { + byte[] b = new byte[bytes]; + r.nextBytes(b); + return b; + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java b/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..fe21666e06641800e38fdc5673b8847818d716b9 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/ImmediateExecutor.java @@ -0,0 +1,10 @@ +package net.sf.briar.plugins; + +import java.util.concurrent.Executor; + +public class ImmediateExecutor implements Executor { + + public void execute(Runnable r) { + r.run(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java b/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f7bad8c34eefa2a6b83e613f5fcd38b7ceab4923 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/PluginManagerImplTest.java @@ -0,0 +1,63 @@ +package net.sf.briar.plugins; + +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.android.AndroidExecutor; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.lifecycle.ShutdownManager; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionDispatcher; +import net.sf.briar.api.ui.UiCallback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public class PluginManagerImplTest extends BriarTestCase { + + @SuppressWarnings("unchecked") + @Test + public void testStartAndStop() throws Exception { + Mockery context = new Mockery(); + final AndroidExecutor androidExecutor = + context.mock(AndroidExecutor.class); + final ShutdownManager shutdownManager = + context.mock(ShutdownManager.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + final Poller poller = context.mock(Poller.class); + final ConnectionDispatcher dispatcher = + context.mock(ConnectionDispatcher.class); + final UiCallback uiCallback = context.mock(UiCallback.class); + context.checking(new Expectations() {{ + // Start + oneOf(poller).start(with(any(Collection.class))); + allowing(db).getConfig(with(any(TransportId.class))); + will(returnValue(new TransportConfig())); + allowing(db).getLocalProperties(with(any(TransportId.class))); + will(returnValue(new TransportProperties())); + allowing(db).getRemoteProperties(with(any(TransportId.class))); + will(returnValue(new TransportProperties())); + allowing(db).mergeLocalProperties(with(any(TransportId.class)), + with(any(TransportProperties.class))); + // Stop + oneOf(poller).stop(); + oneOf(androidExecutor).shutdown(); + }}); + ExecutorService executor = Executors.newCachedThreadPool(); + PluginManagerImpl p = new PluginManagerImpl(executor, androidExecutor, + shutdownManager, db, poller, dispatcher, uiCallback); + // We expect either 3 or 4 plugins to be started, depending on whether + // the test machine has a Bluetooth device + int started = p.start(null); + int stopped = p.stop(); + assertEquals(started, stopped); + assertTrue(started >= 3); + assertTrue(started <= 4); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..402b9509f1f98fc5a59283b3e1591ec0196406d0 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothClientTest.java @@ -0,0 +1,44 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.plugins.DuplexClientTest; + +// This is not a JUnit test - it has to be run manually while the server test +// is running on another machine +public class BluetoothClientTest extends DuplexClientTest { + + private BluetoothClientTest(Executor executor, String serverAddress) { + // Store the server's Bluetooth address and UUID + TransportProperties p = new TransportProperties(); + p.put("address", serverAddress); + p.put("uuid", BluetoothTest.getUuid()); + Map<ContactId, TransportProperties> remote = + Collections.singletonMap(contactId, p); + // Create the plugin + callback = new ClientCallback(new TransportConfig(), + new TransportProperties(), remote); + plugin = new BluetoothPlugin(executor, new SystemClock(), callback, 0L); + } + + public static void main(String[] args) throws Exception { + if(args.length != 1) { + System.err.println("Please specify the server's Bluetooth address"); + System.exit(1); + } + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new BluetoothClientTest(executor, args[0]).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d59dda8f59af52b74d2a9d782bbb1b2c26926574 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothServerTest.java @@ -0,0 +1,35 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.clock.SystemClock; +import net.sf.briar.plugins.DuplexServerTest; + +// This is not a JUnit test - it has to be run manually while the client test +// is running on another machine +public class BluetoothServerTest extends DuplexServerTest { + + private BluetoothServerTest(Executor executor) { + // Store the UUID + TransportProperties local = new TransportProperties(); + local.put("uuid", BluetoothTest.getUuid()); + // Create the plugin + callback = new ServerCallback(new TransportConfig(), local, + Collections.singletonMap(contactId, new TransportProperties())); + plugin = new BluetoothPlugin(executor, new SystemClock(), callback, 0L); + } + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new BluetoothServerTest(executor).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java new file mode 100644 index 0000000000000000000000000000000000000000..17676bb08b254671e72d8fcb9d94d47db3d4ff95 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/bluetooth/BluetoothTest.java @@ -0,0 +1,13 @@ +package net.sf.briar.plugins.bluetooth; + +import java.util.UUID; + +class BluetoothTest { + + private static final String EMPTY_UUID = + UUID.nameUUIDFromBytes(new byte[0]).toString().replaceAll("-", ""); + + static String getUuid() { + return EMPTY_UUID; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java b/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6634adc6577557ce1f18b6e64abcd01a27554102 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/LinuxRemovableDriveFinderTest.java @@ -0,0 +1,25 @@ +package net.sf.briar.plugins.file; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class LinuxRemovableDriveFinderTest extends BriarTestCase { + + @Test + public void testParseMountPoint() { + LinuxRemovableDriveFinder f = new LinuxRemovableDriveFinder(); + String line = "/dev/sda3 on / type ext3" + + " (rw,errors=remount-ro,commit=0)"; + assertEquals("/", f.parseMountPoint(line)); + line = "gvfs-fuse-daemon on /home/alice/.gvfs" + + " type fuse.gvfs-fuse-daemon (rw,nosuid,nodev,user=alice)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "fusectl on /sys/fs/fuse/connections type fusectl (rw)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "/dev/sdd1 on /media/HAZ SPACE(!) type vfat" + + " (rw,nosuid,nodev,uhelper=udisks,uid=1000,gid=1000," + + "shortname=mixed,dmask=0077,utf8=1,showexec,flush)"; + assertEquals("/media/HAZ SPACE(!)", f.parseMountPoint(line)); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java b/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5b0fae4701e049704cdde5aedc95c68a19c930a2 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/MacRemovableDriveFinderTest.java @@ -0,0 +1,23 @@ +package net.sf.briar.plugins.file; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class MacRemovableDriveFinderTest extends BriarTestCase { + + @Test + public void testParseMountPoint() { + MacRemovableDriveFinder f = new MacRemovableDriveFinder(); + String line = "/dev/disk0s3 on / (local, journaled)"; + assertEquals("/", f.parseMountPoint(line)); + line = "devfs on /dev (local)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "<volfs> on /.vol"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "automount -nsl [117] on /Network (automounted)"; + assertEquals(null, f.parseMountPoint(line)); // Can't be parsed + line = "/dev/disk1s1 on /Volumes/HAZ SPACE(!) (local, nodev, nosuid)"; + assertEquals("/Volumes/HAZ SPACE(!)", f.parseMountPoint(line)); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java b/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f4968b8e1c2f0a8296edf952850e0e4d9af7ca96 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/PollingRemovableDriveMonitorTest.java @@ -0,0 +1,95 @@ +package net.sf.briar.plugins.file; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; + +import org.junit.Test; + +public class PollingRemovableDriveMonitorTest extends BriarTestCase { + + @Test + public void testOneCallbackPerFile() throws Exception { + // Create a finder that returns no files the first time, then two files + final File file1 = new File("foo"); + final File file2 = new File("bar"); + final RemovableDriveFinder finder = new RemovableDriveFinder() { + + private AtomicBoolean firstCall = new AtomicBoolean(true); + + public Collection<File> findRemovableDrives() throws IOException { + if(firstCall.getAndSet(false)) return Collections.emptyList(); + else return Arrays.asList(file1, file2); + } + }; + // Create a callback that waits for two files + final CountDownLatch latch = new CountDownLatch(2); + final List<File> detected = new ArrayList<File>(); + Callback callback = new Callback() { + + public void driveInserted(File f) { + detected.add(f); + latch.countDown(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }; + // Create the monitor and start it + final RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor( + Executors.newCachedThreadPool(), finder, 1); + monitor.start(callback); + // Wait for the monitor to detect the files + assertTrue(latch.await(10, SECONDS)); + monitor.stop(); + // Check that both files were detected + assertEquals(2, detected.size()); + assertTrue(detected.contains(file1)); + assertTrue(detected.contains(file2)); + } + + @Test + public void testExceptionCallback() throws Exception { + // Create a finder that throws an exception the second time it's polled + final RemovableDriveFinder finder = new RemovableDriveFinder() { + + private AtomicBoolean firstCall = new AtomicBoolean(true); + + public Collection<File> findRemovableDrives() throws IOException { + if(firstCall.getAndSet(false)) return Collections.emptyList(); + else throw new IOException(); + } + }; + // Create a callback that waits for an exception + final CountDownLatch latch = new CountDownLatch(1); + Callback callback = new Callback() { + + public void driveInserted(File root) { + fail(); + } + + public void exceptionThrown(IOException e) { + latch.countDown(); + } + }; + // Create the monitor and start it + final RemovableDriveMonitor monitor = new PollingRemovableDriveMonitor( + Executors.newCachedThreadPool(), finder, 1); + monitor.start(callback); + assertTrue(latch.await(10, SECONDS)); + monitor.stop(); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java b/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..f7cbe893becdc5b754983549bc4bee7595bbb0d4 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/RemovableDrivePluginTest.java @@ -0,0 +1,355 @@ +package net.sf.briar.plugins.file; + +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executor; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.plugins.simplex.SimplexPluginCallback; +import net.sf.briar.api.plugins.simplex.SimplexTransportWriter; +import net.sf.briar.plugins.ImmediateExecutor; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class RemovableDrivePluginTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + private final ContactId contactId = new ContactId(234); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testWriterIsNullIfNoDrivesAreFound() throws Exception { + final List<File> drives = Collections.emptyList(); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfNoDriveIsChosen() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List<File> drives = new ArrayList<File>(); + drives.add(drive1); + drives.add(drive2); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(-1)); // The user cancelled the choice + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfOutputDirDoesNotExist() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List<File> drives = new ArrayList<File>(); + drives.add(drive1); + drives.add(drive2); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 but it doesn't exist + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNullIfOutputDirIsAFile() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List<File> drives = new ArrayList<File>(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a file rather than a directory + assertTrue(drive1.createNewFile()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 but it's not a dir + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNull(plugin.createWriter(contactId)); + File[] files = drive1.listFiles(); + assertTrue(files == null || files.length == 0); + + context.assertIsSatisfied(); + } + + @Test + public void testWriterIsNotNullIfOutputDirIsADir() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List<File> drives = new ArrayList<File>(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a directory + assertTrue(drive1.mkdir()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + assertNotNull(plugin.createWriter(contactId)); + // The output file should exist and should be empty + File[] files = drive1.listFiles(); + assertNotNull(files); + assertEquals(1, files.length); + assertEquals(0L, files[0].length()); + + context.assertIsSatisfied(); + } + + @Test + public void testWritingToWriter() throws Exception { + final File drive1 = new File(testDir, "1"); + final File drive2 = new File(testDir, "2"); + final List<File> drives = new ArrayList<File>(); + drives.add(drive1); + drives.add(drive2); + // Create drive1 as a directory + assertTrue(drive1.mkdir()); + + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(finder).findRemovableDrives(); + will(returnValue(drives)); + oneOf(callback).showChoice(with(any(String[].class)), + with(any(String.class))); + will(returnValue(0)); // The user chose drive1 + oneOf(callback).showMessage(with(any(String.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + SimplexTransportWriter writer = plugin.createWriter(contactId); + assertNotNull(writer); + // The output file should exist and should be empty + File[] files = drive1.listFiles(); + assertNotNull(files); + assertEquals(1, files.length); + assertEquals(0L, files[0].length()); + // Writing to the output stream should increase the size of the file + OutputStream out = writer.getOutputStream(); + out.write(new byte[123]); + out.flush(); + out.close(); + // Disposing of the writer should not delete the file + writer.dispose(false); + assertTrue(files[0].exists()); + assertEquals(123L, files[0].length()); + + context.assertIsSatisfied(); + } + + @Test + public void testEmptyDriveIsIgnored() throws Exception { + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + plugin.start(); + + plugin.driveInserted(testDir); + + context.assertIsSatisfied(); + } + + @Test + public void testFilenames() { + Mockery context = new Mockery(); + final Executor executor = context.mock(Executor.class); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor, + callback, finder, monitor); + + assertFalse(plugin.isPossibleConnectionFilename("abcdefg.dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefghi.dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefgh_dat")); + assertFalse(plugin.isPossibleConnectionFilename("abcdefgh.rat")); + assertTrue(plugin.isPossibleConnectionFilename("abcdefgh.dat")); + assertTrue(plugin.isPossibleConnectionFilename("ABCDEFGH.DAT")); + + context.assertIsSatisfied(); + } + + @Test + public void testReaderIsCreated() throws Exception { + Mockery context = new Mockery(); + final SimplexPluginCallback callback = + context.mock(SimplexPluginCallback.class); + final RemovableDriveFinder finder = + context.mock(RemovableDriveFinder.class); + final RemovableDriveMonitor monitor = + context.mock(RemovableDriveMonitor.class); + + context.checking(new Expectations() {{ + oneOf(monitor).start(with(any(Callback.class))); + oneOf(callback).readerCreated(with(any(FileTransportReader.class))); + }}); + + RemovableDrivePlugin plugin = new RemovableDrivePlugin( + new ImmediateExecutor(), callback, finder, monitor); + plugin.start(); + + File f = new File(testDir, "abcdefgh.dat"); + OutputStream out = new FileOutputStream(f); + out.write(new byte[MIN_CONNECTION_LENGTH]); + out.flush(); + out.close(); + assertEquals(MIN_CONNECTION_LENGTH, f.length()); + plugin.driveInserted(testDir); + + context.assertIsSatisfied(); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java b/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..17bdba120e11d81c695d6fe00d0bfa5e8c611bdb --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/file/UnixRemovableDriveMonitorTest.java @@ -0,0 +1,100 @@ +package net.sf.briar.plugins.file; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.plugins.file.RemovableDriveMonitor.Callback; +import net.sf.briar.util.OsUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class UnixRemovableDriveMonitorTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testNonexistentDir() throws Exception { + if(!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) { + System.err.println("Warning: Skipping test"); + return; + } + File doesNotExist = new File(testDir, "doesNotExist"); + RemovableDriveMonitor monitor = createMonitor(doesNotExist); + monitor.start(new Callback() { + + public void driveInserted(File root) { + fail(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }); + monitor.stop(); + } + + @Test + public void testOneCallbackPerFile() throws Exception { + if(!(OsUtils.isLinux() || OsUtils.isMacLeopardOrNewer())) { + System.err.println("Warning: Skipping test"); + return; + } + // Create a callback that will wait for two files before stopping + final List<File> detected = new ArrayList<File>(); + final CountDownLatch latch = new CountDownLatch(2); + final Callback callback = new Callback() { + + public void driveInserted(File f) { + detected.add(f); + latch.countDown(); + } + + public void exceptionThrown(IOException e) { + fail(); + } + }; + // Create the monitor and start it + RemovableDriveMonitor monitor = createMonitor(testDir); + monitor.start(callback); + // Create two files in the test directory + File file1 = new File(testDir, "1"); + File file2 = new File(testDir, "2"); + assertTrue(file1.createNewFile()); + assertTrue(file2.createNewFile()); + // Wait for the monitor to detect the files + assertTrue(latch.await(5, SECONDS)); + monitor.stop(); + // Check that both files were detected + assertEquals(2, detected.size()); + assertTrue(detected.contains(file1)); + assertTrue(detected.contains(file2)); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } + + private RemovableDriveMonitor createMonitor(final File dir) { + return new UnixRemovableDriveMonitor() { + @Override + protected String[] getPathsToWatch() { + return new String[] { dir.getPath() }; + } + }; + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..da1f8641da00889e4781e76bcc9ac563f1e65a8a --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpClientTest.java @@ -0,0 +1,45 @@ +package net.sf.briar.plugins.tcp; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.plugins.DuplexClientTest; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +// This is not a JUnit test - it has to be run manually while the server test +// is running on another machine +public class LanTcpClientTest extends DuplexClientTest { + + private LanTcpClientTest(Executor executor, String serverAddress, + String serverPort) { + // Store the server's internal address and port + TransportProperties p = new TransportProperties(); + p.put("internal", serverAddress); + p.put("port", serverPort); + Map<ContactId, TransportProperties> remote = + Collections.singletonMap(contactId, p); + // Create the plugin + callback = new ClientCallback(new TransportConfig(), + new TransportProperties(), remote); + plugin = new LanTcpPlugin(executor, callback, 0L); + } + + public static void main(String[] args) throws Exception { + if(args.length != 2) { + System.err.println("Please specify the server's address and port"); + System.exit(1); + } + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new LanTcpClientTest(executor, args[0], args[1]).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7c2a1350fc0b3c552457fdbe0acad55510b4136b --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpPluginTest.java @@ -0,0 +1,142 @@ +package net.sf.briar.plugins.tcp; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPlugin; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +import org.junit.Test; + +public class LanTcpPluginTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + + @Test + public void testIncomingConnection() throws Exception { + Callback callback = new Callback(); + callback.local.put("address", "127.0.0.1"); + callback.local.put("port", "0"); + Executor e = Executors.newCachedThreadPool(); + DuplexPlugin plugin = new LanTcpPlugin(e, callback, 0L); + plugin.start(); + // The plugin should have bound a socket and stored the port number + assertTrue(callback.propertiesLatch.await(5, SECONDS)); + String host = callback.local.get("address"); + assertNotNull(host); + assertEquals("127.0.0.1", host); + String portString = callback.local.get("port"); + assertNotNull(portString); + int port = Integer.valueOf(portString); + assertTrue(port > 0 && port < 65536); + // The plugin should be listening on the port + InetSocketAddress addr = new InetSocketAddress(host, port); + Socket s = new Socket(); + s.connect(addr, 100); + assertTrue(callback.connectionsLatch.await(5, SECONDS)); + s.close(); + // Stop the plugin + plugin.stop(); + } + + @Test + public void testOutgoingConnection() throws Exception { + Callback callback = new Callback(); + Executor e = Executors.newCachedThreadPool(); + DuplexPlugin plugin = new LanTcpPlugin(e, callback, 0L); + plugin.start(); + // Listen on a local port + final ServerSocket ss = new ServerSocket(); + ss.bind(new InetSocketAddress("127.0.0.1", 0), 10); + int port = ss.getLocalPort(); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean error = new AtomicBoolean(false); + new Thread() { + @Override + public void run() { + try { + ss.accept(); + latch.countDown(); + } catch(IOException e) { + error.set(true); + } + } + }.start(); + // Tell the plugin about the port + TransportProperties p = new TransportProperties(); + p.put("address", "127.0.0.1"); + p.put("port", String.valueOf(port)); + callback.remote.put(contactId, p); + // Connect to the port + DuplexTransportConnection d = plugin.createConnection(contactId); + assertNotNull(d); + // Check that the connection was accepted + assertTrue(latch.await(5, SECONDS)); + assertFalse(error.get()); + // Clean up + d.dispose(false, true); + ss.close(); + plugin.stop(); + } + + private static class Callback implements DuplexPluginCallback { + + private final Map<ContactId, TransportProperties> remote = + new Hashtable<ContactId, TransportProperties>(); + private final CountDownLatch propertiesLatch = new CountDownLatch(1); + private final CountDownLatch connectionsLatch = new CountDownLatch(1); + private final TransportProperties local = new TransportProperties(); + + public TransportConfig getConfig() { + return new TransportConfig(); + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map<ContactId, TransportProperties> getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) {} + + public void mergeLocalProperties(TransportProperties p) { + local.putAll(p); + propertiesLatch.countDown(); + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + connectionsLatch.countDown(); + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5efe68677d499178a1d93ba9ca13b6ed301271b8 --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tcp/LanTcpServerTest.java @@ -0,0 +1,32 @@ +package net.sf.briar.plugins.tcp; + +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.plugins.DuplexServerTest; +import net.sf.briar.plugins.tcp.LanTcpPlugin; + +// This is not a JUnit test - it has to be run manually while the client test +// is running on another machine +public class LanTcpServerTest extends DuplexServerTest { + + private LanTcpServerTest(Executor executor) { + callback = new ServerCallback(new TransportConfig(), + new TransportProperties(), + Collections.singletonMap(contactId, new TransportProperties())); + plugin = new LanTcpPlugin(executor, callback, 0L); + } + + public static void main(String[] args) throws Exception { + ExecutorService executor = Executors.newCachedThreadPool(); + try { + new LanTcpServerTest(executor).run(); + } finally { + executor.shutdown(); + } + } +} diff --git a/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java b/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a876cb496c5c9ef15bf2975489b97fe538ba0a2c --- /dev/null +++ b/briar-tests/src/net/sf/briar/plugins/tor/TorPluginTest.java @@ -0,0 +1,175 @@ +package net.sf.briar.plugins.tor; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.io.PrintStream; +import java.util.Hashtable; +import java.util.Map; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.TransportConfig; +import net.sf.briar.api.TransportProperties; +import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; +import net.sf.briar.api.plugins.duplex.DuplexTransportConnection; + +import org.junit.Test; + +public class TorPluginTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + + @Test + public void testHiddenService() throws Exception { + System.err.println("======== testHiddenService ========"); + Executor e = Executors.newCachedThreadPool(); + TorPlugin serverPlugin = null, clientPlugin = null; + try { + // Create a plugin instance for the server + Callback serverCallback = new Callback(); + serverPlugin = new TorPlugin(e, serverCallback, 0L); + System.out.println("Starting server plugin"); + serverPlugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(serverCallback.latch.await(600, SECONDS)); + System.out.println("Started server plugin"); + String onion = serverCallback.local.get("onion"); + assertNotNull(onion); + assertTrue(onion.endsWith(".onion")); + // Create another plugin instance for the client + Callback clientCallback = new Callback(); + clientCallback.config.put("noHiddenService", ""); + TransportProperties p = new TransportProperties(); + p.put("onion", onion); + clientCallback.remote.put(contactId, p); + clientPlugin = new TorPlugin(e, clientCallback, 0L); + System.out.println("Starting client plugin"); + clientPlugin.start(); + // The plugin should start without creating a hidden service + assertTrue(clientCallback.latch.await(600, SECONDS)); + System.out.println("Started client plugin"); + // Connect to the server's hidden service + System.out.println("Connecting to hidden service"); + DuplexTransportConnection clientEnd = + clientPlugin.createConnection(contactId); + assertNotNull(clientEnd); + DuplexTransportConnection serverEnd = + serverCallback.incomingConnection; + assertNotNull(serverEnd); + System.out.println("Connected to hidden service"); + // Send some data through the Tor connection + PrintStream out = new PrintStream(clientEnd.getOutputStream()); + out.println("Hello world"); + out.flush(); + Scanner in = new Scanner(serverEnd.getInputStream()); + assertTrue(in.hasNextLine()); + assertEquals("Hello world", in.nextLine()); + serverEnd.dispose(false, false); + clientEnd.dispose(false, false); + } finally { + // Stop the plugins + System.out.println("Stopping plugins"); + if(serverPlugin != null) serverPlugin.stop(); + if(clientPlugin != null) clientPlugin.stop(); + System.out.println("Stopped plugins"); + } + } + + @Test + public void testStoreAndRetrievePrivateKey() throws Exception { + System.err.println("======== testStoreAndRetrievePrivateKey ========"); + Executor e = Executors.newCachedThreadPool(); + TorPlugin plugin = null; + try { + // Start a plugin instance with no private key + Callback callback = new Callback(); + plugin = new TorPlugin(e, callback, 0L); + System.out.println("Starting plugin without private key"); + plugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(callback.latch.await(600, SECONDS)); + System.out.println("Started plugin"); + String onion = callback.local.get("onion"); + assertNotNull(onion); + assertTrue(onion.endsWith(".onion")); + // Get the PEM-encoded private key + String privateKey = callback.config.get("privateKey"); + assertNotNull(privateKey); + // Stop the plugin + System.out.println("Stopping plugin"); + plugin.stop(); + System.out.println("Stopped plugin"); + // Start another instance, reusing the private key + callback = new Callback(); + callback.config.put("privateKey", privateKey); + plugin = new TorPlugin(e, callback, 0L); + System.out.println("Starting plugin with private key"); + plugin.start(); + // The plugin should create a hidden service... eventually + assertTrue(callback.latch.await(600, SECONDS)); + System.out.println("Started plugin"); + // The onion URL should be the same + assertEquals(onion, callback.local.get("onion")); + // The private key should be the same + assertEquals(privateKey, callback.config.get("privateKey")); + } finally { + // Stop the plugin + System.out.println("Stopping plugin"); + if(plugin != null) plugin.stop(); + System.out.println("Stopped plugin"); + } + } + + private static class Callback implements DuplexPluginCallback { + + private final Map<ContactId, TransportProperties> remote = + new Hashtable<ContactId, TransportProperties>(); + private final CountDownLatch latch = new CountDownLatch(1); + private final TransportConfig config = new TransportConfig(); + private final TransportProperties local = new TransportProperties(); + + private volatile DuplexTransportConnection incomingConnection = null; + + public TransportConfig getConfig() { + return config; + } + + public TransportProperties getLocalProperties() { + return local; + } + + public Map<ContactId, TransportProperties> getRemoteProperties() { + return remote; + } + + public void mergeConfig(TransportConfig c) { + config.putAll(c); + } + + public void mergeLocalProperties(TransportProperties p) { + local.putAll(p); + latch.countDown(); + } + + public int showChoice(String[] options, String... message) { + return -1; + } + + public boolean showConfirmationMessage(String... message) { + return false; + } + + public void showMessage(String... message) {} + + public void incomingConnectionCreated(DuplexTransportConnection d) { + incomingConnection = d; + } + + public void outgoingConnectionCreated(ContactId c, + DuplexTransportConnection d) {} + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java b/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a40a1f297db378d80beac2db7677c570253e4925 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/AckReaderTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collection; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class AckReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final SerialComponent serial; + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + + public AckReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + serial = i.getInstance(SerialComponent.class); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfAckIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + + byte[] b = createAck(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + try { + reader.readStruct(Types.ACK, Ack.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + @SuppressWarnings("unchecked") + public void testNoFormatExceptionIfAckIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + final Ack ack = context.mock(Ack.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createAck(with(any(Collection.class))); + will(returnValue(ack)); + }}); + + byte[] b = createAck(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + assertEquals(ack, reader.readStruct(Types.ACK, Ack.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyAck() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + AckReader ackReader = new AckReader(packetFactory); + + byte[] b = createEmptyAck(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.ACK, ackReader); + + try { + reader.readStruct(Types.ACK, Ack.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createAck(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.ACK); + w.writeListStart(); + while(out.size() + serial.getSerialisedUniqueIdLength() + < MAX_PACKET_LENGTH) { + w.writeBytes(TestUtils.getRandomId()); + } + if(tooBig) w.writeBytes(TestUtils.getRandomId()); + w.writeListEnd(); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createEmptyAck() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.ACK); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java b/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6323638b01297da9aeab597cc301e6bb54e24342 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/BatchReaderTest.java @@ -0,0 +1,137 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.protocol.UnverifiedBatch; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.StructReader; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class BatchReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + private final UnverifiedMessage message; + private final StructReader<UnverifiedMessage> messageReader; + + public BatchReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + message = context.mock(UnverifiedMessage.class); + messageReader = new TestMessageReader(); + } + + @Test + public void testFormatExceptionIfBatchIsTooLarge() throws Exception { + UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + + byte[] b = createBatch(MAX_PACKET_LENGTH + 1); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + try { + reader.readStruct(Types.BATCH, UnverifiedBatch.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + public void testNoFormatExceptionIfBatchIsMaximumSize() throws Exception { + final UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + final UnverifiedBatch batch = context.mock(UnverifiedBatch.class); + context.checking(new Expectations() {{ + oneOf(batchFactory).createUnverifiedBatch( + Collections.singletonList(message)); + will(returnValue(batch)); + }}); + + byte[] b = createBatch(MAX_PACKET_LENGTH); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + assertEquals(batch, reader.readStruct(Types.BATCH, + UnverifiedBatch.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyBatch() throws Exception { + final UnverifiedBatchFactory batchFactory = + context.mock(UnverifiedBatchFactory.class); + BatchReader batchReader = new BatchReader(messageReader, batchFactory); + + byte[] b = createEmptyBatch(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.BATCH, batchReader); + + try { + reader.readStruct(Types.BATCH, UnverifiedBatch.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createBatch(int size) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(size); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.BATCH); + w.writeListStart(); + // We're using a fake message reader, so it's OK to use a fake message + w.writeStructId(Types.MESSAGE); + w.writeBytes(new byte[size - 10]); + w.writeListEnd(); + byte[] b = out.toByteArray(); + assertEquals(size, b.length); + return b; + } + + private byte[] createEmptyBatch() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.BATCH); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } + + private class TestMessageReader implements StructReader<UnverifiedMessage> { + + public UnverifiedMessage readStruct(Reader r) throws IOException { + r.readStructId(Types.MESSAGE); + r.readBytes(); + return message; + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e4c6af3a1a9a52e32c31819b974701b0521addd4 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ConstantsTest.java @@ -0,0 +1,193 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_AUTHOR_NAME_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_BODY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_GROUP_NAME_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTIES_PER_TRANSPORT; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PROPERTY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PUBLIC_KEY_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_SUBJECT_LENGTH; +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_TRANSPORTS; + +import java.io.ByteArrayOutputStream; +import java.security.PrivateKey; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +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.BatchId; +import net.sf.briar.api.protocol.Group; +import net.sf.briar.api.protocol.GroupFactory; +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.ProtocolWriter; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +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.protocol.UniqueId; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ConstantsTest extends BriarTestCase { + + private final CryptoComponent crypto; + private final GroupFactory groupFactory; + private final AuthorFactory authorFactory; + private final MessageFactory messageFactory; + private final PacketFactory packetFactory; + private final ProtocolWriterFactory protocolWriterFactory; + + public ConstantsTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + crypto = i.getInstance(CryptoComponent.class); + groupFactory = i.getInstance(GroupFactory.class); + authorFactory = i.getInstance(AuthorFactory.class); + messageFactory = i.getInstance(MessageFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class); + } + + @Test + public void testBatchesFitIntoLargeAck() throws Exception { + testBatchesFitIntoAck(MAX_PACKET_LENGTH); + } + + @Test + public void testBatchesFitIntoSmallAck() throws Exception { + testBatchesFitIntoAck(1000); + } + + private void testBatchesFitIntoAck(int length) throws Exception { + // Create an ack with as many batch IDs as possible + ByteArrayOutputStream out = new ByteArrayOutputStream(length); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + int maxBatches = writer.getMaxBatchesForAck(length); + Collection<BatchId> acked = new ArrayList<BatchId>(); + for(int i = 0; i < maxBatches; i++) { + acked.add(new BatchId(TestUtils.getRandomId())); + } + Ack a = packetFactory.createAck(acked); + writer.writeAck(a); + // Check the size of the serialised ack + assertTrue(out.size() <= length); + } + + @Test + public void testMessageFitsIntoBatch() throws Exception { + // Create a maximum-length group + String groupName = createRandomString(MAX_GROUP_NAME_LENGTH); + byte[] groupPublic = new byte[MAX_PUBLIC_KEY_LENGTH]; + Group group = groupFactory.createGroup(groupName, groupPublic); + // Create a maximum-length author + String authorName = createRandomString(MAX_AUTHOR_NAME_LENGTH); + byte[] authorPublic = new byte[MAX_PUBLIC_KEY_LENGTH]; + Author author = authorFactory.createAuthor(authorName, authorPublic); + // Create a maximum-length message + PrivateKey groupPrivate = crypto.generateSignatureKeyPair().getPrivate(); + PrivateKey authorPrivate = crypto.generateSignatureKeyPair().getPrivate(); + String subject = createRandomString(MAX_SUBJECT_LENGTH); + byte[] body = new byte[MAX_BODY_LENGTH]; + Message message = messageFactory.createMessage(null, group, + groupPrivate, author, authorPrivate, subject, body); + // Add the message to a batch + ByteArrayOutputStream out = + new ByteArrayOutputStream(MAX_PACKET_LENGTH); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + RawBatch b = packetFactory.createBatch(Collections.singletonList( + message.getSerialised())); + writer.writeBatch(b); + // Check the size of the serialised batch + assertTrue(out.size() > UniqueId.LENGTH + MAX_GROUP_NAME_LENGTH + + MAX_PUBLIC_KEY_LENGTH + MAX_AUTHOR_NAME_LENGTH + + MAX_PUBLIC_KEY_LENGTH + MAX_BODY_LENGTH); + assertTrue(out.size() <= MAX_PACKET_LENGTH); + } + + @Test + public void testMessagesFitIntoLargeOffer() throws Exception { + testMessagesFitIntoOffer(MAX_PACKET_LENGTH); + } + + @Test + public void testMessagesFitIntoSmallOffer() throws Exception { + testMessagesFitIntoOffer(1000); + } + + private void testMessagesFitIntoOffer(int length) throws Exception { + // Create an offer with as many message IDs as possible + ByteArrayOutputStream out = new ByteArrayOutputStream(length); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + int maxMessages = writer.getMaxMessagesForOffer(length); + Collection<MessageId> offered = new ArrayList<MessageId>(); + for(int i = 0; i < maxMessages; i++) { + offered.add(new MessageId(TestUtils.getRandomId())); + } + Offer o = packetFactory.createOffer(offered); + writer.writeOffer(o); + // Check the size of the serialised offer + assertTrue(out.size() <= length); + } + + @Test + public void testTransportsFitIntoUpdate() throws Exception { + // Create the maximum number of plugins, each with the maximum number + // of maximum-length properties + Collection<Transport> transports = new ArrayList<Transport>(); + for(int i = 0; i < MAX_TRANSPORTS; i++) { + TransportId id = new TransportId(TestUtils.getRandomId()); + Transport t = new Transport(id); + for(int j = 0; j < MAX_PROPERTIES_PER_TRANSPORT; j++) { + String key = createRandomString(MAX_PROPERTY_LENGTH); + String value = createRandomString(MAX_PROPERTY_LENGTH); + t.put(key, value); + } + transports.add(t); + } + // Add the transports to an update + ByteArrayOutputStream out = + new ByteArrayOutputStream(MAX_PACKET_LENGTH); + ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out, + true); + TransportUpdate t = packetFactory.createTransportUpdate(transports, + Long.MAX_VALUE); + writer.writeTransportUpdate(t); + // Check the size of the serialised update + assertTrue(out.size() > MAX_TRANSPORTS * (UniqueId.LENGTH + 4 + + (MAX_PROPERTIES_PER_TRANSPORT * MAX_PROPERTY_LENGTH * 2)) + + 8); + assertTrue(out.size() <= MAX_PACKET_LENGTH); + } + + private static String createRandomString(int length) throws Exception { + StringBuilder s = new StringBuilder(length); + for(int i = 0; i < length; i++) { + int digit = (int) (Math.random() * 10); + s.append((char) ('0' + digit)); + } + String string = s.toString(); + assertEquals(length, string.getBytes("UTF-8").length); + return string; + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java b/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0bb89d6d29a3fe4e0a79e9bd2b561691fd978cdb --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ConsumersTest.java @@ -0,0 +1,105 @@ +package net.sf.briar.protocol; + +import static org.junit.Assert.assertArrayEquals; + +import java.security.GeneralSecurityException; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.crypto.MessageDigest; +import net.sf.briar.api.serial.CopyingConsumer; +import net.sf.briar.api.serial.CountingConsumer; +import net.sf.briar.api.serial.DigestingConsumer; + +import org.junit.Test; + +public class ConsumersTest extends BriarTestCase { + + @Test + public void testDigestingConsumer() throws Exception { + byte[] data = new byte[1234]; + // Generate some random data and digest it + new Random().nextBytes(data); + MessageDigest messageDigest = new TestMessageDigest(); + messageDigest.update(data); + byte[] dig = messageDigest.digest(); + // Check that feeding a DigestingConsumer generates the same digest + DigestingConsumer dc = new DigestingConsumer(messageDigest); + dc.write(data[0]); + dc.write(data, 1, data.length - 2); + dc.write(data[data.length - 1]); + byte[] dig1 = messageDigest.digest(); + assertArrayEquals(dig, dig1); + } + + @Test + public void testCountingConsumer() throws Exception { + byte[] data = new byte[1234]; + CountingConsumer cc = new CountingConsumer(data.length); + cc.write(data[0]); + cc.write(data, 1, data.length - 2); + cc.write(data[data.length - 1]); + assertEquals(data.length, cc.getCount()); + try { + cc.write((byte) 0); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testCopyingConsumer() throws Exception { + byte[] data = new byte[1234]; + new Random().nextBytes(data); + // Check that a CopyingConsumer creates a faithful copy + CopyingConsumer cc = new CopyingConsumer(); + cc.write(data[0]); + cc.write(data, 1, data.length - 2); + cc.write(data[data.length - 1]); + assertArrayEquals(data, cc.getCopy()); + } + + private static class TestMessageDigest implements MessageDigest { + + private final java.security.MessageDigest delegate; + + private TestMessageDigest() throws GeneralSecurityException { + delegate = java.security.MessageDigest.getInstance("SHA-256"); + } + + public byte[] digest() { + return delegate.digest(); + } + + public byte[] digest(byte[] input) { + return delegate.digest(input); + } + + public int digest(byte[] buf, int offset, int len) { + byte[] digest = digest(); + len = Math.min(len, digest.length); + System.arraycopy(digest, 0, buf, offset, len); + return len; + } + + public int getDigestLength() { + return delegate.getDigestLength(); + } + + public void reset() { + delegate.reset(); + } + + public void update(byte input) { + delegate.update(input); + } + + public void update(byte[] input) { + delegate.update(input); + } + + public void update(byte[] input, int offset, int len) { + delegate.update(input, offset, len); + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java b/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d3ecbc0fbc96e098dd52182e7317c03a123f6755 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/OfferReaderTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.Collection; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.Offer; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class OfferReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final SerialComponent serial; + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final Mockery context; + + public OfferReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new SerialModule()); + serial = i.getInstance(SerialComponent.class); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfOfferIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + + byte[] b = createOffer(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + try { + reader.readStruct(Types.OFFER, Offer.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + @SuppressWarnings("unchecked") + public void testNoFormatExceptionIfOfferIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + final Offer offer = context.mock(Offer.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createOffer(with(any(Collection.class))); + will(returnValue(offer)); + }}); + + byte[] b = createOffer(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + assertEquals(offer, reader.readStruct(Types.OFFER, Offer.class)); + context.assertIsSatisfied(); + } + + @Test + public void testEmptyOffer() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + OfferReader offerReader = new OfferReader(packetFactory); + + byte[] b = createEmptyOffer(); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.OFFER, offerReader); + + try { + reader.readStruct(Types.OFFER, Offer.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + private byte[] createOffer(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.OFFER); + w.writeListStart(); + while(out.size() + serial.getSerialisedUniqueIdLength() + < MAX_PACKET_LENGTH) { + w.writeBytes(TestUtils.getRandomId()); + } + if(tooBig) w.writeBytes(TestUtils.getRandomId()); + w.writeListEnd(); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createEmptyOffer() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.OFFER); + w.writeListStart(); + w.writeListEnd(); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4c939bc9c40a38a5b3e2afe0ad8f88e3415b12b8 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ProtocolIntegrationTest.java @@ -0,0 +1,133 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.protocol.Ack; +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.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.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ProtocolIntegrationTest extends BriarTestCase { + + private final ProtocolReaderFactory readerFactory; + private final ProtocolWriterFactory writerFactory; + private final PacketFactory packetFactory; + private final BatchId batchId; + private final Group group; + private final Message message; + private final String subject = "Hello"; + private final String messageBody = "Hello world"; + private final BitSet bitSet; + private final Map<Group, Long> subscriptions; + private final Collection<Transport> transports; + private final long timestamp = System.currentTimeMillis(); + + public ProtocolIntegrationTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + readerFactory = i.getInstance(ProtocolReaderFactory.class); + writerFactory = i.getInstance(ProtocolWriterFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + batchId = new BatchId(TestUtils.getRandomId()); + GroupFactory groupFactory = i.getInstance(GroupFactory.class); + group = groupFactory.createGroup("Unrestricted group", null); + MessageFactory messageFactory = i.getInstance(MessageFactory.class); + message = messageFactory.createMessage(null, group, subject, + messageBody.getBytes("UTF-8")); + bitSet = new BitSet(); + bitSet.set(3); + bitSet.set(7); + subscriptions = Collections.singletonMap(group, 123L); + 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 { + // Write + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter writer = writerFactory.createProtocolWriter(out, true); + + Ack a = packetFactory.createAck(Collections.singletonList(batchId)); + writer.writeAck(a); + + RawBatch b = packetFactory.createBatch(Collections.singletonList( + message.getSerialised())); + writer.writeBatch(b); + + Offer o = packetFactory.createOffer(Collections.singletonList( + message.getId())); + writer.writeOffer(o); + + Request r = packetFactory.createRequest(bitSet, 10); + writer.writeRequest(r); + + SubscriptionUpdate s = packetFactory.createSubscriptionUpdate( + Collections.<GroupId, GroupId>emptyMap(), subscriptions, 0L, + timestamp); + writer.writeSubscriptionUpdate(s); + + TransportUpdate t = packetFactory.createTransportUpdate(transports, + timestamp); + writer.writeTransportUpdate(t); + + // Read + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + ProtocolReader reader = readerFactory.createProtocolReader(in); + + a = reader.readAck(); + assertEquals(Collections.singletonList(batchId), a.getBatchIds()); + + Batch b1 = reader.readBatch().verify(); + assertEquals(Collections.singletonList(message), b1.getMessages()); + + o = reader.readOffer(); + assertEquals(Collections.singletonList(message.getId()), + o.getMessageIds()); + + r = reader.readRequest(); + assertEquals(bitSet, r.getBitmap()); + assertEquals(10, r.getLength()); + + s = reader.readSubscriptionUpdate(); + assertEquals(subscriptions, s.getSubscriptions()); + assertEquals(timestamp, s.getTimestamp()); + + t = reader.readTransportUpdate(); + assertEquals(transports, t.getTransports()); + assertEquals(timestamp, t.getTimestamp()); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java b/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4f2343b19a38dc3fd25ffb7fa6c412642047efa8 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/ProtocolWriterImplTest.java @@ -0,0 +1,87 @@ +package net.sf.briar.protocol; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.BitSet; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.ProtocolWriter; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.serial.SerialComponent; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.util.StringUtils; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class ProtocolWriterImplTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final PacketFactory packetFactory; + private final SerialComponent serial; + private final WriterFactory writerFactory; + + public ProtocolWriterImplTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + packetFactory = i.getInstance(PacketFactory.class); + serial = i.getInstance(SerialComponent.class); + writerFactory = i.getInstance(WriterFactory.class); + } + + @Test + public void testWriteBitmapNoPadding() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter w = new ProtocolWriterImpl(serial, writerFactory, out, + true); + BitSet b = new BitSet(); + // 11011001 = 0xD9 + b.set(0); + b.set(1); + b.set(3); + b.set(4); + b.set(7); + // 01011001 = 0x59 + b.set(9); + b.set(11); + b.set(12); + b.set(15); + Request r = packetFactory.createRequest(b, 16); + w.writeRequest(r); + // Short user tag 6, 0 as uint7, short bytes with length 2, 0xD959 + byte[] output = out.toByteArray(); + assertEquals("C6" + "00" + "92" + "D959", + StringUtils.toHexString(output)); + } + + @Test + public void testWriteBitmapWithPadding() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ProtocolWriter w = new ProtocolWriterImpl(serial, writerFactory, out, + true); + BitSet b = new BitSet(); + // 01011001 = 0x59 + b.set(1); + b.set(3); + b.set(4); + b.set(7); + // 11011xxx = 0xD8, after padding + b.set(8); + b.set(9); + b.set(11); + b.set(12); + Request r = packetFactory.createRequest(b, 13); + w.writeRequest(r); + // Short user tag 6, 3 as uint7, short bytes with length 2, 0x59D8 + byte[] output = out.toByteArray(); + assertEquals("C6" + "03" + "92" + "59D8", + StringUtils.toHexString(output)); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java b/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7dc377ecd411d94ea21bf11ad17e35feadfc47c1 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/RequestReaderTest.java @@ -0,0 +1,146 @@ +package net.sf.briar.protocol; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.BitSet; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.protocol.PacketFactory; +import net.sf.briar.api.protocol.Request; +import net.sf.briar.api.protocol.Types; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.api.serial.ReaderFactory; +import net.sf.briar.api.serial.Writer; +import net.sf.briar.api.serial.WriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.serial.SerialModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class RequestReaderTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final ReaderFactory readerFactory; + private final WriterFactory writerFactory; + private final PacketFactory packetFactory; + private final Mockery context; + + public RequestReaderTest() throws Exception { + super(); + Injector i = Guice.createInjector(new CryptoModule(), + new ProtocolModule(), new SerialModule()); + readerFactory = i.getInstance(ReaderFactory.class); + writerFactory = i.getInstance(WriterFactory.class); + packetFactory = i.getInstance(PacketFactory.class); + context = new Mockery(); + } + + @Test + public void testFormatExceptionIfRequestIsTooLarge() throws Exception { + PacketFactory packetFactory = context.mock(PacketFactory.class); + RequestReader requestReader = new RequestReader(packetFactory); + + byte[] b = createRequest(true); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.REQUEST, requestReader); + + try { + reader.readStruct(Types.REQUEST, Request.class); + fail(); + } catch(FormatException expected) {} + context.assertIsSatisfied(); + } + + @Test + public void testNoFormatExceptionIfRequestIsMaximumSize() throws Exception { + final PacketFactory packetFactory = context.mock(PacketFactory.class); + RequestReader requestReader = new RequestReader(packetFactory); + final Request request = context.mock(Request.class); + context.checking(new Expectations() {{ + oneOf(packetFactory).createRequest(with(any(BitSet.class)), + with(any(int.class))); + will(returnValue(request)); + }}); + + byte[] b = createRequest(false); + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + reader.addStructReader(Types.REQUEST, requestReader); + + assertEquals(request, reader.readStruct(Types.REQUEST, + Request.class)); + context.assertIsSatisfied(); + } + + @Test + public void testBitmapDecoding() throws Exception { + // Test sizes from 0 to 1000 bits + for(int i = 0; i < 1000; i++) { + // Create a BitSet of size i with one in ten bits set (on average) + BitSet requested = new BitSet(i); + for(int j = 0; j < i; j++) if(Math.random() < 0.1) requested.set(j); + // Encode the BitSet as a bitmap + int bytes = i % 8 == 0 ? i / 8 : i / 8 + 1; + byte[] bitmap = new byte[bytes]; + for(int j = 0; j < i; j++) { + if(requested.get(j)) { + int offset = j / 8; + byte bit = (byte) (128 >> j % 8); + bitmap[offset] |= bit; + } + } + // Create a serialised request containing the bitmap + byte[] b = createRequest(bitmap); + // Deserialise the request + ByteArrayInputStream in = new ByteArrayInputStream(b); + Reader reader = readerFactory.createReader(in); + RequestReader requestReader = new RequestReader(packetFactory); + reader.addStructReader(Types.REQUEST, requestReader); + Request r = reader.readStruct(Types.REQUEST, Request.class); + BitSet decoded = r.getBitmap(); + // Check that the decoded BitSet matches the original - we can't + // use equals() because of padding, but the first i bits should + // match and the cardinalities should be equal, indicating that no + // padding bits are set + for(int j = 0; j < i; j++) { + assertEquals(requested.get(j), decoded.get(j)); + } + assertEquals(requested.cardinality(), decoded.cardinality()); + } + } + + private byte[] createRequest(boolean tooBig) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.REQUEST); + // Allow one byte for the REQUEST tag, one byte for the padding length + // as a uint7, one byte for the BYTES tag, and five bytes for the + // length of the byte array as an int32 + int size = MAX_PACKET_LENGTH - 8; + if(tooBig) size++; + assertTrue(size > Short.MAX_VALUE); + w.writeUint7((byte) 0); + w.writeBytes(new byte[size]); + assertEquals(tooBig, out.size() > MAX_PACKET_LENGTH); + return out.toByteArray(); + } + + private byte[] createRequest(byte[] bitmap) throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Writer w = writerFactory.createWriter(out); + w.writeStructId(Types.REQUEST); + w.writeUint7((byte) 0); + w.writeBytes(bitmap); + return out.toByteArray(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java b/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..126f8a983aa3894f385a1f694ce90d8afb90cda1 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/UnverifiedBatchImplTest.java @@ -0,0 +1,244 @@ +package net.sf.briar.protocol; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.Signature; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.MessageDigest; +import net.sf.briar.api.protocol.Author; +import net.sf.briar.api.protocol.AuthorId; +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.GroupId; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageId; +import net.sf.briar.api.protocol.UnverifiedBatch; +import net.sf.briar.crypto.CryptoModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class UnverifiedBatchImplTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final CryptoComponent crypto; + private final byte[] raw, raw1; + private final String subject; + private final long timestamp; + + public UnverifiedBatchImplTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + Random r = new Random(); + raw = new byte[123]; + r.nextBytes(raw); + raw1 = new byte[1234]; + r.nextBytes(raw1); + subject = "Unit tests are exciting"; + timestamp = System.currentTimeMillis(); + } + + @Test + public void testIds() throws Exception { + // Calculate the expected batch and message IDs + MessageDigest messageDigest = crypto.getMessageDigest(); + messageDigest.update(raw); + messageDigest.update(raw1); + BatchId batchId = new BatchId(messageDigest.digest()); + messageDigest.update(raw); + MessageId messageId = new MessageId(messageDigest.digest()); + messageDigest.update(raw1); + MessageId messageId1 = new MessageId(messageDigest.digest()); + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(null)); + oneOf(message).getGroup(); + will(returnValue(null)); + oneOf(message).getParent(); + will(returnValue(null)); + oneOf(message).getSubject(); + will(returnValue(subject)); + oneOf(message).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message).getBodyStart(); + will(returnValue(10)); + oneOf(message).getBodyLength(); + will(returnValue(100)); + // Second message + oneOf(message1).getRaw(); + will(returnValue(raw1)); + oneOf(message1).getAuthor(); + will(returnValue(null)); + oneOf(message1).getGroup(); + will(returnValue(null)); + oneOf(message1).getParent(); + will(returnValue(null)); + oneOf(message1).getSubject(); + will(returnValue(subject)); + oneOf(message1).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message1).getBodyStart(); + will(returnValue(10)); + oneOf(message1).getBodyLength(); + will(returnValue(1000)); + }}); + Collection<UnverifiedMessage> messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + Batch verifiedBatch = batch.verify(); + // Check that the batch and message IDs match + assertEquals(batchId, verifiedBatch.getId()); + Collection<Message> verifiedMessages = verifiedBatch.getMessages(); + assertEquals(2, verifiedMessages.size()); + Iterator<Message> it = verifiedMessages.iterator(); + Message verifiedMessage = it.next(); + assertEquals(messageId, verifiedMessage.getId()); + Message verifiedMessage1 = it.next(); + assertEquals(messageId1, verifiedMessage1.getId()); + context.assertIsSatisfied(); + } + + @Test + public void testSignatures() throws Exception { + final int signedByAuthor = 100, signedByGroup = 110; + final KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); + final KeyPair groupKeyPair = crypto.generateSignatureKeyPair(); + Signature signature = crypto.getSignature(); + // Calculate the expected author and group signatures + signature.initSign(authorKeyPair.getPrivate()); + signature.update(raw, 0, signedByAuthor); + final byte[] authorSignature = signature.sign(); + signature.initSign(groupKeyPair.getPrivate()); + signature.update(raw, 0, signedByGroup); + final byte[] groupSignature = signature.sign(); + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final Author author = context.mock(Author.class); + final Group group = context.mock(Group.class); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(author)); + oneOf(author).getPublicKey(); + will(returnValue(authorKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByAuthor(); + will(returnValue(signedByAuthor)); + oneOf(message).getAuthorSignature(); + will(returnValue(authorSignature)); + oneOf(message).getGroup(); + will(returnValue(group)); + exactly(2).of(group).getPublicKey(); + will(returnValue(groupKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByGroup(); + will(returnValue(signedByGroup)); + oneOf(message).getGroupSignature(); + will(returnValue(groupSignature)); + oneOf(author).getId(); + will(returnValue(new AuthorId(TestUtils.getRandomId()))); + oneOf(group).getId(); + will(returnValue(new GroupId(TestUtils.getRandomId()))); + oneOf(message).getParent(); + will(returnValue(null)); + oneOf(message).getSubject(); + will(returnValue(subject)); + oneOf(message).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message).getBodyStart(); + will(returnValue(10)); + oneOf(message).getBodyLength(); + will(returnValue(100)); + // Second message + oneOf(message1).getRaw(); + will(returnValue(raw1)); + oneOf(message1).getAuthor(); + will(returnValue(null)); + oneOf(message1).getGroup(); + will(returnValue(null)); + oneOf(message1).getParent(); + will(returnValue(null)); + oneOf(message1).getSubject(); + will(returnValue(subject)); + oneOf(message1).getTimestamp(); + will(returnValue(timestamp)); + oneOf(message1).getBodyStart(); + will(returnValue(10)); + oneOf(message1).getBodyLength(); + will(returnValue(1000)); + }}); + Collection<UnverifiedMessage> messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + batch.verify(); + context.assertIsSatisfied(); + } + + @Test + public void testExceptionThrownIfMessageIsModified() throws Exception { + final int signedByAuthor = 100; + final KeyPair authorKeyPair = crypto.generateSignatureKeyPair(); + Signature signature = crypto.getSignature(); + // Calculate the expected author signature + signature.initSign(authorKeyPair.getPrivate()); + signature.update(raw, 0, signedByAuthor); + final byte[] authorSignature = signature.sign(); + // Modify the message + raw[signedByAuthor / 2] ^= 0xff; + // Verify the batch + Mockery context = new Mockery(); + final UnverifiedMessage message = + context.mock(UnverifiedMessage.class, "message"); + final Author author = context.mock(Author.class); + final UnverifiedMessage message1 = + context.mock(UnverifiedMessage.class, "message1"); + context.checking(new Expectations() {{ + // First message - verification will fail at the author's signature + oneOf(message).getRaw(); + will(returnValue(raw)); + oneOf(message).getAuthor(); + will(returnValue(author)); + oneOf(author).getPublicKey(); + will(returnValue(authorKeyPair.getPublic().getEncoded())); + oneOf(message).getLengthSignedByAuthor(); + will(returnValue(signedByAuthor)); + oneOf(message).getAuthorSignature(); + will(returnValue(authorSignature)); + }}); + Collection<UnverifiedMessage> messages = Arrays.asList(message, + message1); + UnverifiedBatch batch = new UnverifiedBatchImpl(crypto, messages); + try { + batch.verify(); + fail(); + } catch(GeneralSecurityException expected) {} + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java new file mode 100644 index 0000000000000000000000000000000000000000..bc0491b2e0b5fea2e27b87bdb440df9588f2201b --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java @@ -0,0 +1,177 @@ +package net.sf.briar.protocol.simplex; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayOutputStream; +import java.util.Collections; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.DatabaseExecutor; +import net.sf.briar.api.protocol.Ack; +import net.sf.briar.api.protocol.BatchId; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +import net.sf.briar.api.protocol.RawBatch; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.protocol.UniqueId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionRegistry; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.clock.ClockModule; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.protocol.ProtocolModule; +import net.sf.briar.protocol.duplex.DuplexProtocolModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.transport.TransportModule; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +public class OutgoingSimplexConnectionTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private final Mockery context; + private final DatabaseComponent db; + private final ConnectionRegistry connRegistry; + private final ConnectionWriterFactory connFactory; + private final ProtocolWriterFactory protoFactory; + private final ContactId contactId; + private final TransportId transportId; + private final byte[] secret; + + public OutgoingSimplexConnectionTest() { + super(); + context = new Mockery(); + db = context.mock(DatabaseComponent.class); + Module testModule = new AbstractModule() { + @Override + public void configure() { + bind(DatabaseComponent.class).toInstance(db); + bind(Executor.class).annotatedWith( + DatabaseExecutor.class).toInstance( + Executors.newCachedThreadPool()); + } + }; + Injector i = Guice.createInjector(testModule, new ClockModule(), + new CryptoModule(), new SerialModule(), new TransportModule(), + new SimplexProtocolModule(), new ProtocolModule(), + new DuplexProtocolModule()); + connRegistry = i.getInstance(ConnectionRegistry.class); + connFactory = i.getInstance(ConnectionWriterFactory.class); + protoFactory = i.getInstance(ProtocolWriterFactory.class); + contactId = new ContactId(234); + transportId = new TransportId(TestUtils.getRandomId()); + secret = new byte[32]; + } + + @Test + public void testConnectionTooShort() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MAX_PACKET_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + connection.write(); + // Nothing should have been written + assertEquals(0, out.size()); + // The transport should have been disposed with exception == true + assertTrue(transport.getDisposed()); + assertTrue(transport.getException()); + } + + @Test + public void testNothingToSend() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MIN_CONNECTION_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + context.checking(new Expectations() {{ + // No transports to send + oneOf(db).generateTransportUpdate(contactId); + will(returnValue(null)); + // No subscriptions to send + oneOf(db).generateSubscriptionUpdate(contactId); + will(returnValue(null)); + // No acks to send + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(null)); + // No batches to send + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(null)); + }}); + connection.write(); + // Nothing should have been written + assertEquals(0, out.size()); + // The transport should have been disposed with exception == false + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + context.assertIsSatisfied(); + } + + @Test + public void testSomethingToSend() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, MIN_CONNECTION_LENGTH, true); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + final Ack ack = context.mock(Ack.class); + final BatchId batchId = new BatchId(TestUtils.getRandomId()); + final RawBatch batch = context.mock(RawBatch.class); + final byte[] message = new byte[1234]; + context.checking(new Expectations() {{ + // No transports to send + oneOf(db).generateTransportUpdate(contactId); + will(returnValue(null)); + // No subscriptions to send + oneOf(db).generateSubscriptionUpdate(contactId); + will(returnValue(null)); + // One ack to send + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(ack)); + oneOf(ack).getBatchIds(); + will(returnValue(Collections.singletonList(batchId))); + // No more acks + oneOf(db).generateAck(with(contactId), with(any(int.class))); + will(returnValue(null)); + // One batch to send + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(batch)); + oneOf(batch).getMessages(); + will(returnValue(Collections.singletonList(message))); + // No more batches + oneOf(db).generateBatch(with(contactId), with(any(int.class))); + will(returnValue(null)); + }}); + connection.write(); + // Something should have been written + int overhead = TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH; + assertTrue(out.size() > overhead + UniqueId.LENGTH + message.length); + // The transport should have been disposed with exception == false + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..302b37e3d0f9ef4d948fb6c56164b309a0c528f3 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/SimplexProtocolIntegrationTest.java @@ -0,0 +1,223 @@ +package net.sf.briar.protocol.simplex; + +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestDatabaseModule; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.KeyManager; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.db.event.DatabaseEvent; +import net.sf.briar.api.db.event.DatabaseListener; +import net.sf.briar.api.db.event.MessagesAddedEvent; +import net.sf.briar.api.protocol.Message; +import net.sf.briar.api.protocol.MessageFactory; +import net.sf.briar.api.protocol.ProtocolReaderFactory; +import net.sf.briar.api.protocol.ProtocolWriterFactory; +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.ConnectionReaderFactory; +import net.sf.briar.api.transport.ConnectionRecogniser; +import net.sf.briar.api.transport.ConnectionRegistry; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.api.transport.ContactTransport; +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.plugins.ImmediateExecutor; +import net.sf.briar.protocol.ProtocolModule; +import net.sf.briar.protocol.duplex.DuplexProtocolModule; +import net.sf.briar.serial.SerialModule; +import net.sf.briar.transport.TransportModule; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class SimplexProtocolIntegrationTest extends BriarTestCase { + + private static final long CLOCK_DIFFERENCE = 60 * 1000L; + private static final long LATENCY = 60 * 1000L; + + private final File testDir = TestUtils.getTestDirectory(); + private final File aliceDir = new File(testDir, "alice"); + private final File bobDir = new File(testDir, "bob"); + private final TransportId transportId; + private final byte[] initialSecret; + private final long epoch; + + private Injector alice, bob; + + public SimplexProtocolIntegrationTest() throws Exception { + super(); + transportId = new TransportId(TestUtils.getRandomId()); + // Create matching secrets for Alice and Bob + initialSecret = new byte[32]; + new Random().nextBytes(initialSecret); + long rotationPeriod = 2 * CLOCK_DIFFERENCE + LATENCY; + epoch = System.currentTimeMillis() - 2 * rotationPeriod; + } + + @Before + public void setUp() { + testDir.mkdirs(); + alice = createInjector(aliceDir); + bob = createInjector(bobDir); + } + + private Injector createInjector(File dir) { + return Guice.createInjector(new ClockModule(), new CryptoModule(), + new DatabaseModule(), new LifecycleModule(), + new ProtocolModule(), new SerialModule(), + new TestDatabaseModule(dir), new SimplexProtocolModule(), + new TransportModule(), new DuplexProtocolModule()); + } + + @Test + public void testInjection() { + DatabaseComponent aliceDb = alice.getInstance(DatabaseComponent.class); + DatabaseComponent bobDb = bob.getInstance(DatabaseComponent.class); + assertFalse(aliceDb == bobDb); + } + + @Test + public void testWriteAndRead() throws Exception { + read(write()); + } + + private byte[] write() throws Exception { + // Open Alice's database + DatabaseComponent db = alice.getInstance(DatabaseComponent.class); + db.open(false); + // Start Alice's key manager + KeyManager km = alice.getInstance(KeyManager.class); + km.start(); + // Add Bob as a contact + ContactId contactId = db.addContact(); + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, CLOCK_DIFFERENCE, LATENCY, true); + db.addContactTransport(ct); + km.contactTransportAdded(ct, initialSecret.clone()); + // Send Bob a message + String subject = "Hello"; + byte[] body = "Hi Bob!".getBytes("UTF-8"); + MessageFactory messageFactory = alice.getInstance(MessageFactory.class); + Message message = messageFactory.createMessage(null, subject, body); + db.addLocalPrivateMessage(message, contactId); + // Create an outgoing simplex connection + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ConnectionRegistry connRegistry = + alice.getInstance(ConnectionRegistry.class); + ConnectionWriterFactory connFactory = + alice.getInstance(ConnectionWriterFactory.class); + ProtocolWriterFactory protoFactory = + alice.getInstance(ProtocolWriterFactory.class); + TestSimplexTransportWriter transport = new TestSimplexTransportWriter( + out, Long.MAX_VALUE, false); + ConnectionContext ctx = km.getConnectionContext(contactId, transportId); + assertNotNull(ctx); + OutgoingSimplexConnection simplex = new OutgoingSimplexConnection(db, + connRegistry, connFactory, protoFactory, ctx, transport); + // Write whatever needs to be written + simplex.write(); + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + // Clean up + km.stop(); + db.close(); + // Return the contents of the simplex connection + return out.toByteArray(); + } + + private void read(byte[] b) throws Exception { + // Open Bob's database + DatabaseComponent db = bob.getInstance(DatabaseComponent.class); + db.open(false); + // Start Bob's key manager + KeyManager km = bob.getInstance(KeyManager.class); + km.start(); + // Add Alice as a contact + ContactId contactId = db.addContact(); + ContactTransport ct = new ContactTransport(contactId, transportId, + epoch, CLOCK_DIFFERENCE, LATENCY, false); + db.addContactTransport(ct); + km.contactTransportAdded(ct, initialSecret.clone()); + // Set up a database listener + MessageListener listener = new MessageListener(); + db.addListener(listener); + // Fake a transport update from Alice + TransportUpdate transportUpdate = new TransportUpdate() { + + public Collection<Transport> getTransports() { + Transport t = new Transport(transportId); + return Collections.singletonList(t); + } + + public long getTimestamp() { + return System.currentTimeMillis(); + } + }; + db.receiveTransportUpdate(contactId, transportUpdate); + // Create a connection recogniser and recognise the connection + ByteArrayInputStream in = new ByteArrayInputStream(b); + ConnectionRecogniser rec = bob.getInstance(ConnectionRecogniser.class); + byte[] tag = new byte[TAG_LENGTH]; + int read = in.read(tag); + assertEquals(tag.length, read); + ConnectionContext ctx = rec.acceptConnection(transportId, tag); + assertNotNull(ctx); + // Create an incoming simplex connection + ConnectionRegistry connRegistry = + bob.getInstance(ConnectionRegistry.class); + ConnectionReaderFactory connFactory = + bob.getInstance(ConnectionReaderFactory.class); + ProtocolReaderFactory protoFactory = + bob.getInstance(ProtocolReaderFactory.class); + TestSimplexTransportReader transport = + new TestSimplexTransportReader(in); + IncomingSimplexConnection simplex = new IncomingSimplexConnection( + new ImmediateExecutor(), new ImmediateExecutor(), db, + connRegistry, connFactory, protoFactory, ctx, transport); + // No messages should have been added yet + assertFalse(listener.messagesAdded); + // Read whatever needs to be read + simplex.read(); + assertTrue(transport.getDisposed()); + assertFalse(transport.getException()); + assertTrue(transport.getRecognised()); + // The private message from Alice should have been added + assertTrue(listener.messagesAdded); + // Clean up + km.stop(); + db.close(); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } + + private static class MessageListener implements DatabaseListener { + + private boolean messagesAdded = false; + + public void eventOccurred(DatabaseEvent e) { + if(e instanceof MessagesAddedEvent) + messagesAdded = true; + } + } +} diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java new file mode 100644 index 0000000000000000000000000000000000000000..1d85ed3ec57599ea7e3e49b273bf1e09313c4db3 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportReader.java @@ -0,0 +1,39 @@ +package net.sf.briar.protocol.simplex; + +import java.io.InputStream; + +import net.sf.briar.api.plugins.simplex.SimplexTransportReader; + +class TestSimplexTransportReader implements SimplexTransportReader { + + private final InputStream in; + + private boolean disposed = false, exception = false, recognised = false; + + TestSimplexTransportReader(InputStream in) { + this.in = in; + } + + public InputStream getInputStream() { + return in; + } + + public void dispose(boolean exception, boolean recognised) { + assert !disposed; + disposed = true; + this.exception = exception; + this.recognised = recognised; + } + + boolean getDisposed() { + return disposed; + } + + boolean getException() { + return exception; + } + + boolean getRecognised() { + return recognised; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..7a1524cb5761d6e43899a4f1e6860183dd4451c2 --- /dev/null +++ b/briar-tests/src/net/sf/briar/protocol/simplex/TestSimplexTransportWriter.java @@ -0,0 +1,48 @@ +package net.sf.briar.protocol.simplex; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import net.sf.briar.api.plugins.simplex.SimplexTransportWriter; + +class TestSimplexTransportWriter implements SimplexTransportWriter { + + private final ByteArrayOutputStream out; + private final long capacity; + private final boolean flush; + + private boolean disposed = false, exception = false; + + TestSimplexTransportWriter(ByteArrayOutputStream out, long capacity, + boolean flush) { + this.out = out; + this.capacity = capacity; + this.flush = flush; + } + + public long getCapacity() { + return capacity; + } + + public OutputStream getOutputStream() { + return out; + } + + public boolean shouldFlush() { + return flush; + } + + public void dispose(boolean exception) { + assert !disposed; + disposed = true; + this.exception = exception; + } + + boolean getDisposed() { + return disposed; + } + + boolean getException() { + return exception; + } +} \ No newline at end of file diff --git a/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a79cdb56b8661484bea66ae1f1e8664876cd7513 --- /dev/null +++ b/briar-tests/src/net/sf/briar/serial/ReaderImplTest.java @@ -0,0 +1,556 @@ +package net.sf.briar.serial; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.Bytes; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.serial.Consumer; +import net.sf.briar.api.serial.StructReader; +import net.sf.briar.api.serial.Reader; +import net.sf.briar.util.StringUtils; + +import org.junit.Test; + +public class ReaderImplTest extends BriarTestCase { + + private ByteArrayInputStream in = null; + private ReaderImpl r = null; + + @Test + public void testReadBoolean() throws Exception { + setContents("FFFE"); + assertFalse(r.readBoolean()); + assertTrue(r.readBoolean()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt8() throws Exception { + setContents("FD00" + "FDFF" + "FD7F" + "FD80"); + assertEquals((byte) 0, r.readInt8()); + assertEquals((byte) -1, r.readInt8()); + assertEquals(Byte.MAX_VALUE, r.readInt8()); + assertEquals(Byte.MIN_VALUE, r.readInt8()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt16() throws Exception { + setContents("FC0000" + "FCFFFF" + "FC7FFF" + "FC8000"); + assertEquals((short) 0, r.readInt16()); + assertEquals((short) -1, r.readInt16()); + assertEquals(Short.MAX_VALUE, r.readInt16()); + assertEquals(Short.MIN_VALUE, r.readInt16()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt32() throws Exception { + setContents("FB00000000" + "FBFFFFFFFF" + "FB7FFFFFFF" + "FB80000000"); + assertEquals(0, r.readInt32()); + assertEquals(-1, r.readInt32()); + assertEquals(Integer.MAX_VALUE, r.readInt32()); + assertEquals(Integer.MIN_VALUE, r.readInt32()); + assertTrue(r.eof()); + } + + @Test + public void testReadInt64() throws Exception { + setContents("FA0000000000000000" + "FAFFFFFFFFFFFFFFFF" + + "FA7FFFFFFFFFFFFFFF" + "FA8000000000000000"); + assertEquals(0L, r.readInt64()); + assertEquals(-1L, r.readInt64()); + assertEquals(Long.MAX_VALUE, r.readInt64()); + assertEquals(Long.MIN_VALUE, r.readInt64()); + assertTrue(r.eof()); + } + + @Test + public void testReadIntAny() throws Exception { + setContents("00" + "7F" + "FD80" + "FDFF" + "FC0080" + "FC7FFF" + + "FB00008000" + "FB7FFFFFFF" + "FA0000000080000000"); + assertEquals(0L, r.readIntAny()); + assertEquals(127L, r.readIntAny()); + assertEquals(-128L, r.readIntAny()); + assertEquals(-1L, r.readIntAny()); + assertEquals(128L, r.readIntAny()); + assertEquals(32767L, r.readIntAny()); + assertEquals(32768L, r.readIntAny()); + assertEquals(2147483647L, r.readIntAny()); + assertEquals(2147483648L, r.readIntAny()); + assertTrue(r.eof()); + } + + @Test + public void testReadFloat32() throws Exception { + // http://babbage.cs.qc.edu/IEEE-754/Decimal.html + // http://steve.hollasch.net/cgindex/coding/ieeefloat.html + setContents("F900000000" + "F93F800000" + "F940000000" + "F9BF800000" + + "F980000000" + "F9FF800000" + "F97F800000" + "F97FC00000"); + assertEquals(0F, r.readFloat32()); + assertEquals(1F, r.readFloat32()); + assertEquals(2F, r.readFloat32()); + assertEquals(-1F, r.readFloat32()); + assertEquals(-0F, r.readFloat32()); + assertEquals(Float.NEGATIVE_INFINITY, r.readFloat32()); + assertEquals(Float.POSITIVE_INFINITY, r.readFloat32()); + assertTrue(Float.isNaN(r.readFloat32())); + assertTrue(r.eof()); + } + + @Test + public void testReadFloat64() throws Exception { + setContents("F80000000000000000" + "F83FF0000000000000" + + "F84000000000000000" + "F8BFF0000000000000" + + "F88000000000000000" + "F8FFF0000000000000" + + "F87FF0000000000000" + "F87FF8000000000000"); + assertEquals(0.0, r.readFloat64()); + assertEquals(1.0, r.readFloat64()); + assertEquals(2.0, r.readFloat64()); + assertEquals(-1.0, r.readFloat64()); + assertEquals(-0.0, r.readFloat64()); + assertEquals(Double.NEGATIVE_INFINITY, r.readFloat64()); + assertEquals(Double.POSITIVE_INFINITY, r.readFloat64()); + assertTrue(Double.isNaN(r.readFloat64())); + assertTrue(r.eof()); + } + + @Test + public void testReadString() throws Exception { + setContents("F703666F6F" + "83666F6F" + "F700" + "80"); + assertEquals("foo", r.readString()); + assertEquals("foo", r.readString()); + assertEquals("", r.readString()); + assertEquals("", r.readString()); + assertTrue(r.eof()); + } + + @Test + public void testReadStringMaxLength() throws Exception { + setContents("83666F6F" + "83666F6F"); + assertEquals("foo", r.readString(3)); + try { + r.readString(2); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadBytes() throws Exception { + setContents("F603010203" + "93010203" + "F600" + "90"); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes()); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertTrue(r.eof()); + } + + @Test + public void testReadBytesMaxLength() throws Exception { + setContents("93010203" + "93010203"); + assertArrayEquals(new byte[] {1, 2, 3}, r.readBytes(3)); + try { + r.readBytes(2); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadShortList() throws Exception { + setContents("A" + "3" + "01" + "83666F6F" + "FC0080"); + List<Object> l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadList() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + List<Object> l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadListTypeSafe() throws Exception { + setContents("A" + "3" + "01" + "02" + "03"); + List<Byte> l = r.readList(Byte.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals(Byte.valueOf((byte) 1), l.get(0)); + assertEquals(Byte.valueOf((byte) 2), l.get(1)); + assertEquals(Byte.valueOf((byte) 3), l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadListTypeSafeThrowsFormatException() throws Exception { + setContents("A" + "3" + "01" + "83666F6F" + "03"); + // Trying to read a mixed list as a list of bytes should throw a + // FormatException + try { + r.readList(Byte.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadShortMap() throws Exception { + setContents("B" + "2" + "83666F6F" + "7B" + "90" + "F2"); + Map<Object, Object> m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadMap() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + Map<Object, Object> m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadMapTypeSafe() throws Exception { + setContents("B" + "2" + "83666F6F" + "7B" + "80" + "F2"); + Map<String, Byte> m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 123), m.get("foo")); + assertTrue(m.containsKey("")); + assertNull(m.get("")); + assertTrue(r.eof()); + } + + @Test + public void testMapKeysMustBeUnique() throws Exception { + setContents("B" + "2" + "83666F6F" + "01" + "83626172" + "02" + + "B" + "2" + "83666F6F" + "01" + "83666F6F" + "02"); + // The first map has unique keys + Map<String, Byte> m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 1), m.get("foo")); + assertEquals(Byte.valueOf((byte) 2), m.get("bar")); + // The second map has a duplicate key + try { + r.readMap(String.class, Byte.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadDelimitedList() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + List<Object> l = r.readList(Object.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals((byte) 1, l.get(0)); + assertEquals("foo", l.get(1)); + assertEquals((short) 128, l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedListElements() throws Exception { + setContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + assertTrue(r.hasListStart()); + r.readListStart(); + assertFalse(r.hasListEnd()); + assertEquals((byte) 1, r.readIntAny()); + assertFalse(r.hasListEnd()); + assertEquals("foo", r.readString()); + assertFalse(r.hasListEnd()); + assertEquals((short) 128, r.readIntAny()); + assertTrue(r.hasListEnd()); + r.readListEnd(); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedListTypeSafe() throws Exception { + setContents("F5" + "01" + "02" + "03" + "F3"); + List<Byte> l = r.readList(Byte.class); + assertNotNull(l); + assertEquals(3, l.size()); + assertEquals(Byte.valueOf((byte) 1), l.get(0)); + assertEquals(Byte.valueOf((byte) 2), l.get(1)); + assertEquals(Byte.valueOf((byte) 3), l.get(2)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMap() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + Map<Object, Object> m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals((byte) 123, m.get("foo")); + Bytes b = new Bytes(new byte[] {}); + assertTrue(m.containsKey(b)); + assertNull(m.get(b)); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMapEntries() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + assertTrue(r.hasMapStart()); + r.readMapStart(); + assertFalse(r.hasMapEnd()); + assertEquals("foo", r.readString()); + assertFalse(r.hasMapEnd()); + assertEquals((byte) 123, r.readIntAny()); + assertFalse(r.hasMapEnd()); + assertArrayEquals(new byte[] {}, r.readBytes()); + assertFalse(r.hasMapEnd()); + assertTrue(r.hasNull()); + r.readNull(); + assertTrue(r.hasMapEnd()); + r.readMapEnd(); + assertTrue(r.eof()); + } + + @Test + public void testReadDelimitedMapTypeSafe() throws Exception { + setContents("F4" + "83666F6F" + "7B" + "80" + "F2" + "F3"); + Map<String, Byte> m = r.readMap(String.class, Byte.class); + assertNotNull(m); + assertEquals(2, m.size()); + assertEquals(Byte.valueOf((byte) 123), m.get("foo")); + assertTrue(m.containsKey("")); + assertNull(m.get("")); + assertTrue(r.eof()); + } + + @Test + @SuppressWarnings("unchecked") + public void testReadNestedMapsAndLists() throws Exception { + setContents("B" + "1" + "B" + "1" + "83666F6F" + "7B" + + "A" + "1" + "01"); + Map<Object, Object> m = r.readMap(Object.class, Object.class); + assertNotNull(m); + assertEquals(1, m.size()); + Entry<Object, Object> e = m.entrySet().iterator().next(); + Map<Object, Object> m1 = (Map<Object, Object>) e.getKey(); + assertNotNull(m1); + assertEquals(1, m1.size()); + assertEquals((byte) 123, m1.get("foo")); + List<Object> l = (List<Object>) e.getValue(); + assertNotNull(l); + assertEquals(1, l.size()); + assertEquals((byte) 1, l.get(0)); + assertTrue(r.eof()); + } + + @Test + public void testReadStruct() throws Exception { + setContents("C0" + "83666F6F" + "F1" + "FF" + "83666F6F"); + // Add readers for two structs + r.addStructReader(0, new StructReader<Foo>() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(255, new StructReader<Bar>() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(255); + return new Bar(r.readString()); + } + }); + // Test both ID formats, short and long + assertTrue(r.hasStruct(0)); + assertEquals("foo", r.readStruct(0, Foo.class).s); + assertTrue(r.hasStruct(255)); + assertEquals("foo", r.readStruct(255, Bar.class).s); + } + + @Test + public void testReadStructWithConsumer() throws Exception { + setContents("C0" + "83666F6F" + "F1" + "FF" + "83666F6F"); + // Add readers for two structs + r.addStructReader(0, new StructReader<Foo>() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(255, new StructReader<Bar>() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(255); + return new Bar(r.readString()); + } + }); + // Add a consumer + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + r.addConsumer(new Consumer() { + + public void write(byte b) throws IOException { + out.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + }); + // Test both ID formats, short and long + assertTrue(r.hasStruct(0)); + assertEquals("foo", r.readStruct(0, Foo.class).s); + assertTrue(r.hasStruct(255)); + assertEquals("foo", r.readStruct(255, Bar.class).s); + // Check that everything was passed to the consumer + assertEquals("C0" + "83666F6F" + "F1" + "FF" + "83666F6F", + StringUtils.toHexString(out.toByteArray())); + } + + @Test + public void testUnknownStructIdThrowsFormatException() throws Exception { + setContents("C0" + "83666F6F"); + assertTrue(r.hasStruct(0)); + // No reader has been added for struct ID 0 + try { + r.readStruct(0, Foo.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testWrongClassThrowsFormatException() throws Exception { + setContents("C0" + "83666F6F"); + // Add a reader for struct ID 0, class Foo + r.addStructReader(0, new StructReader<Foo>() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + assertTrue(r.hasStruct(0)); + // Trying to read the struct as class Bar should throw a FormatException + try { + r.readStruct(0, Bar.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadListUsingStructReader() throws Exception { + setContents("A" + "1" + "C0" + "83666F6F"); + // Add a reader for a struct + r.addStructReader(0, new StructReader<Foo>() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + // Check that the reader is used for lists + List<Foo> l = r.readList(Foo.class); + assertEquals(1, l.size()); + assertEquals("foo", l.get(0).s); + } + + @Test + public void testReadMapUsingStructReader() throws Exception { + setContents("B" + "1" + "C0" + "83666F6F" + "C1" + "83626172"); + // Add readers for two structs + r.addStructReader(0, new StructReader<Foo>() { + public Foo readStruct(Reader r) throws IOException { + r.readStructId(0); + return new Foo(r.readString()); + } + }); + r.addStructReader(1, new StructReader<Bar>() { + public Bar readStruct(Reader r) throws IOException { + r.readStructId(1); + return new Bar(r.readString()); + } + }); + // Check that the readers are used for maps + Map<Foo, Bar> m = r.readMap(Foo.class, Bar.class); + assertEquals(1, m.size()); + Entry<Foo, Bar> e = m.entrySet().iterator().next(); + assertEquals("foo", e.getKey().s); + assertEquals("bar", e.getValue().s); + } + + @Test + public void testMaxLengthAppliesInsideMap() throws Exception { + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxStringLength(3); + r.setMaxBytesLength(3); + Map<String, Bytes> m = r.readMap(String.class, Bytes.class); + String key = "foo"; + Bytes value = new Bytes(new byte[] {1, 2, 3}); + assertEquals(Collections.singletonMap(key, value), m); + // The max string length should be applied inside the map + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxStringLength(2); + try { + r.readMap(String.class, Bytes.class); + fail(); + } catch(FormatException expected) {} + // The max bytes length should be applied inside the map + setContents("B" + "1" + "83666F6F" + "93010203"); + r.setMaxBytesLength(2); + try { + r.readMap(String.class, Bytes.class); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testReadEmptyInput() throws Exception { + setContents(""); + assertTrue(r.eof()); + } + + private void setContents(String hex) { + in = new ByteArrayInputStream(StringUtils.fromHexString(hex)); + r = new ReaderImpl(in); + } + + private static class Foo { + + private final String s; + + private Foo(String s) { + this.s = s; + } + } + + private static class Bar { + + private final String s; + + private Bar(String s) { + this.s = s; + } + } +} diff --git a/briar-tests/src/net/sf/briar/serial/WriterImplTest.java b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7c9112e9d0fedb68072371a369755f2120266654 --- /dev/null +++ b/briar-tests/src/net/sf/briar/serial/WriterImplTest.java @@ -0,0 +1,291 @@ +package net.sf.briar.serial; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.util.StringUtils; + +import org.junit.Before; +import org.junit.Test; + +public class WriterImplTest extends BriarTestCase { + + private ByteArrayOutputStream out = null; + private WriterImpl w = null; + + @Before + public void setUp() { + out = new ByteArrayOutputStream(); + w = new WriterImpl(out); + } + + @Test + public void testWriteBoolean() throws IOException { + w.writeBoolean(true); + w.writeBoolean(false); + // TRUE tag, FALSE tag + checkContents("FE" + "FF"); + } + + @Test + public void testWriteUint7() throws IOException { + w.writeUint7((byte) 0); + w.writeUint7(Byte.MAX_VALUE); + // 0, 127 + checkContents("00" + "7F"); + } + + @Test + public void testWriteInt8() throws IOException { + w.writeInt8((byte) 0); + w.writeInt8((byte) -1); + w.writeInt8(Byte.MIN_VALUE); + w.writeInt8(Byte.MAX_VALUE); + // INT8 tag, 0, INT8 tag, -1, INT8 tag, -128, INT8 tag, 127 + checkContents("FD" + "00" + "FD" + "FF" + "FD" + "80" + "FD" + "7F"); + } + + @Test + public void testWriteInt16() throws IOException { + w.writeInt16((short) 0); + w.writeInt16((short) -1); + w.writeInt16(Short.MIN_VALUE); + w.writeInt16(Short.MAX_VALUE); + // INT16 tag, 0, INT16 tag, -1, INT16 tag, -32768, INT16 tag, 32767 + checkContents("FC" + "0000" + "FC" + "FFFF" + "FC" + "8000" + + "FC" + "7FFF"); + } + + @Test + public void testWriteInt32() throws IOException { + w.writeInt32(0); + w.writeInt32(-1); + w.writeInt32(Integer.MIN_VALUE); + w.writeInt32(Integer.MAX_VALUE); + // INT32 tag, 0, INT32 tag, -1, etc + checkContents("FB" + "00000000" + "FB" + "FFFFFFFF" + "FB" + "80000000" + + "FB" + "7FFFFFFF"); + } + + @Test + public void testWriteInt64() throws IOException { + w.writeInt64(0L); + w.writeInt64(-1L); + w.writeInt64(Long.MIN_VALUE); + w.writeInt64(Long.MAX_VALUE); + // INT64 tag, 0, INT64 tag, -1, etc + checkContents("FA" + "0000000000000000" + "FA" + "FFFFFFFFFFFFFFFF" + + "FA" + "8000000000000000" + "FA" + "7FFFFFFFFFFFFFFF"); + } + + @Test + public void testWriteIntAny() throws IOException { + w.writeIntAny(0); // uint7 + w.writeIntAny(-1); // int8 + w.writeIntAny(Byte.MAX_VALUE); // uint7 + w.writeIntAny(Byte.MAX_VALUE + 1); // int16 + w.writeIntAny(Short.MAX_VALUE); // int16 + w.writeIntAny(Short.MAX_VALUE + 1); // int32 + w.writeIntAny(Integer.MAX_VALUE); // int32 + w.writeIntAny(Integer.MAX_VALUE + 1L); // int64 + checkContents("00" + "FDFF" + "7F" + "FC0080" + "FC7FFF" + + "FB00008000" + "FB7FFFFFFF" + "FA0000000080000000"); + } + + @Test + public void testWriteFloat32() throws IOException { + // http://babbage.cs.qc.edu/IEEE-754/Decimal.html + // 1 bit for sign, 8 for exponent, 23 for significand + w.writeFloat32(0F); // 0 0 0 -> 0x00000000 + w.writeFloat32(1F); // 0 127 1 -> 0x3F800000 + w.writeFloat32(2F); // 0 128 1 -> 0x40000000 + w.writeFloat32(-1F); // 1 127 1 -> 0xBF800000 + w.writeFloat32(-0F); // 1 0 0 -> 0x80000000 + // http://steve.hollasch.net/cgindex/coding/ieeefloat.html + w.writeFloat32(Float.NEGATIVE_INFINITY); // 1 255 0 -> 0xFF800000 + w.writeFloat32(Float.POSITIVE_INFINITY); // 0 255 0 -> 0x7F800000 + w.writeFloat32(Float.NaN); // 0 255 1 -> 0x7FC00000 + checkContents("F9" + "00000000" + "F9" + "3F800000" + "F9" + "40000000" + + "F9" + "BF800000" + "F9" + "80000000" + "F9" + "FF800000" + + "F9" + "7F800000" + "F9" + "7FC00000"); + } + + @Test + public void testWriteFloat64() throws IOException { + // 1 bit for sign, 11 for exponent, 52 for significand + w.writeFloat64(0.0); // 0 0 0 -> 0x0000000000000000 + w.writeFloat64(1.0); // 0 1023 1 -> 0x3FF0000000000000 + w.writeFloat64(2.0); // 0 1024 1 -> 0x4000000000000000 + w.writeFloat64(-1.0); // 1 1023 1 -> 0xBFF0000000000000 + w.writeFloat64(-0.0); // 1 0 0 -> 0x8000000000000000 + w.writeFloat64(Double.NEGATIVE_INFINITY); // 1 2047 0 -> 0xFFF00000... + w.writeFloat64(Double.POSITIVE_INFINITY); // 0 2047 0 -> 0x7FF00000... + w.writeFloat64(Double.NaN); // 0 2047 1 -> 0x7FF8000000000000 + checkContents("F8" + "0000000000000000" + "F8" + "3FF0000000000000" + + "F8" + "4000000000000000" + "F8" + "BFF0000000000000" + + "F8" + "8000000000000000" + "F8" + "FFF0000000000000" + + "F8" + "7FF0000000000000" + "F8" + "7FF8000000000000"); + } + + @Test + public void testWriteShortString() throws IOException { + w.writeString("foo bar baz bam"); + // SHORT_STRING tag, length 15, UTF-8 bytes + checkContents("8" + "F" + "666F6F206261722062617A2062616D"); + } + + @Test + public void testWriteString() throws IOException { + w.writeString("foo bar baz bam "); + // STRING tag, length 16 as uint7, UTF-8 bytes + checkContents("F7" + "10" + "666F6F206261722062617A2062616D20"); + } + + @Test + public void testWriteShortBytes() throws IOException { + w.writeBytes(new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 + }); + // SHORT_BYTES tag, length 15, bytes + checkContents("9" + "F" + "000102030405060708090A0B0C0D0E"); + } + + @Test + public void testWriteBytes() throws IOException { + w.writeBytes(new byte[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }); + // BYTES tag, length 16 as uint7, bytes + checkContents("F6" + "10" + "000102030405060708090A0B0C0D0E0F"); + } + + @Test + public void testWriteShortList() throws IOException { + List<Object> l = new ArrayList<Object>(); + for(int i = 0; i < 15; i++) l.add(i); + w.writeList(l); + // SHORT_LIST tag, length, elements as uint7 + checkContents("A" + "F" + "000102030405060708090A0B0C0D0E"); + } + + @Test + public void testWriteList() throws IOException { + List<Object> l = new ArrayList<Object>(); + for(int i = 0; i < 16; i++) l.add(i); + w.writeList(l); + // LIST tag, elements as uint7, END tag + checkContents("F5" + "000102030405060708090A0B0C0D0E0F" + "F3"); + } + + @Test + public void testListCanContainNull() throws IOException { + List<Object> l = new ArrayList<Object>(); + l.add(1); + l.add(null); + l.add(2); + w.writeList(l); + // SHORT_LIST tag, length, 1 as uint7, null, 2 as uint7 + checkContents("A" + "3" + "01" + "F2" + "02"); + } + + @Test + public void testWriteShortMap() throws IOException { + // Use LinkedHashMap to get predictable iteration order + Map<Object, Object> m = new LinkedHashMap<Object, Object>(); + for(int i = 0; i < 15; i++) m.put(i, i + 1); + w.writeMap(m); + // SHORT_MAP tag, size, entries as uint7 + checkContents("B" + "F" + "0001" + "0102" + "0203" + "0304" + "0405" + + "0506" + "0607" + "0708" + "0809" + "090A" + "0A0B" + "0B0C" + + "0C0D" + "0D0E" + "0E0F"); + } + + @Test + public void testWriteMap() throws IOException { + // Use LinkedHashMap to get predictable iteration order + Map<Object, Object> m = new LinkedHashMap<Object, Object>(); + for(int i = 0; i < 16; i++) m.put(i, i + 1); + w.writeMap(m); + // MAP tag, entries as uint7, END tag + checkContents("F4" + "0001" + "0102" + "0203" + "0304" + "0405" + + "0506" + "0607" + "0708" + "0809" + "090A" + "0A0B" + "0B0C" + + "0C0D" + "0D0E" + "0E0F" + "0F10" + "F3"); + } + + @Test + public void testWriteDelimitedList() throws IOException { + w.writeListStart(); + w.writeIntAny((byte) 1); // Written as uint7 + w.writeString("foo"); // Written as short string + w.writeIntAny(128L); // Written as an int16 + w.writeListEnd(); + // LIST tag, 1 as uint7, "foo" as short string, 128 as int16, + // END tag + checkContents("F5" + "01" + "83666F6F" + "FC0080" + "F3"); + } + + @Test + public void testWriteDelimitedMap() throws IOException { + w.writeMapStart(); + w.writeString("foo"); // Written as short string + w.writeIntAny(123); // Written as a uint7 + w.writeBytes(new byte[] {}); // Written as short bytes + w.writeNull(); + w.writeMapEnd(); + // MAP tag, "foo" as short string, 123 as uint7, + // byte[] {} as short bytes, NULL tag, END tag + checkContents("F4" + "83666F6F" + "7B" + "90" + "F2" + "F3"); + } + + @Test + public void testWriteNestedMapsAndLists() throws IOException { + Map<Object, Object> m = new LinkedHashMap<Object, Object>(); + m.put("foo", Integer.valueOf(123)); + List<Object> l = new ArrayList<Object>(); + l.add(Byte.valueOf((byte) 1)); + Map<Object, Object> m1 = new LinkedHashMap<Object, Object>(); + m1.put(m, l); + w.writeMap(m1); + // SHORT_MAP tag, length 1, SHORT_MAP tag, length 1, + // "foo" as short string, 123 as uint7, SHORT_LIST tag, length 1, + // 1 as uint7 + checkContents("B" + "1" + "B" + "1" + "83666F6F" + "7B" + "A1" + "01"); + } + + @Test + public void testWriteNull() throws IOException { + w.writeNull(); + checkContents("F2"); + } + + @Test + public void testWriteShortStructId() throws IOException { + w.writeStructId(0); + w.writeStructId(31); + // SHORT_STRUCT tag (3 bits), 0 (5 bits), SHORT_STRUCT tag (3 bits), + // 31 (5 bits) + checkContents("C0" + "DF"); + } + + @Test + public void testWriteStructId() throws IOException { + w.writeStructId(32); + w.writeStructId(255); + // STRUCT tag, 32 as uint8, STRUCT tag, 255 as uint8 + checkContents("F1" + "20" + "F1" + "FF"); + } + + private void checkContents(String hex) throws IOException { + out.flush(); + out.close(); + byte[] expected = StringUtils.fromHexString(hex); + assertTrue(StringUtils.toHexString(out.toByteArray()), + Arrays.equals(expected, out.toByteArray())); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4bec12fc63964be0720a2a6dfcab4fed5439c2ca --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionReaderImplTest.java @@ -0,0 +1,107 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import net.sf.briar.BriarTestCase; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + +public class ConnectionReaderImplTest extends BriarTestCase { + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + @Test + public void testEmptyFramesAreSkipped() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(2)); // Non-empty frame with two payload bytes + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + assertEquals(0, c.read()); // Skip the first empty frame, read a byte + assertEquals(0, c.read()); // Read another byte + assertEquals(-1, c.read()); // Skip the second empty frame, reach EOF + assertEquals(-1, c.read()); // Still at EOF + context.assertIsSatisfied(); + } + + @Test + public void testEmptyFramesAreSkippedWithBuffer() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(2)); // Non-empty frame with two payload bytes + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(0)); // Empty frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH]; + // Skip the first empty frame, read the two payload bytes + assertEquals(2, c.read(buf)); + // Skip the second empty frame, reach EOF + assertEquals(-1, c.read(buf)); + // Still at EOF + assertEquals(-1, c.read(buf)); + context.assertIsSatisfied(); + } + + @Test + public void testMultipleReadsPerFrame() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH / 2]; + // Read the first half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf)); + // Read the second half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf)); + // Reach EOF + assertEquals(-1, c.read(buf, 0, buf.length)); + context.assertIsSatisfied(); + } + + @Test + public void testMultipleReadsPerFrameWithOffsets() throws Exception { + Mockery context = new Mockery(); + final FrameReader reader = context.mock(FrameReader.class); + context.checking(new Expectations() {{ + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame + oneOf(reader).readFrame(with(any(byte[].class))); + will(returnValue(-1)); // No more frames + }}); + ConnectionReaderImpl c = new ConnectionReaderImpl(reader, FRAME_LENGTH); + byte[] buf = new byte[MAX_PAYLOAD_LENGTH]; + // Read the first half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, MAX_PAYLOAD_LENGTH / 2, + MAX_PAYLOAD_LENGTH / 2)); + // Read the second half of the payload + assertEquals(MAX_PAYLOAD_LENGTH / 2, c.read(buf, 123, + MAX_PAYLOAD_LENGTH / 2)); + // Reach EOF + assertEquals(-1, c.read(buf, 0, buf.length)); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..af5e853acc9181ed6c771eab4481c3e356c6307f --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionRegistryImplTest.java @@ -0,0 +1,73 @@ +package net.sf.briar.transport; + +import java.util.Arrays; +import java.util.Collections; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionRegistry; + +import org.junit.Test; + +public class ConnectionRegistryImplTest extends BriarTestCase { + + private final ContactId contactId, contactId1; + private final TransportId transportId, transportId1; + + public ConnectionRegistryImplTest() { + super(); + contactId = new ContactId(1); + contactId1 = new ContactId(2); + transportId = new TransportId(TestUtils.getRandomId()); + transportId1 = new TransportId(TestUtils.getRandomId()); + } + + @Test + public void testRegisterAndUnregister() { + ConnectionRegistry c = new ConnectionRegistryImpl(); + // The registry should be empty + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Check that a registered connection shows up + c.registerConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Register an identical connection - lookup should be unaffected + c.registerConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Unregister one of the connections - lookup should be unaffected + c.unregisterConnection(contactId, transportId); + assertEquals(Collections.singletonList(contactId), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Unregister the other connection - lookup should be affected + c.unregisterConnection(contactId, transportId); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId)); + assertEquals(Collections.emptyList(), + c.getConnectedContacts(transportId1)); + // Try to unregister the connection again - exception should be thrown + try { + c.unregisterConnection(contactId, transportId); + fail(); + } catch(IllegalArgumentException expected) {} + // Register both contacts with one transport, one contact with both + c.registerConnection(contactId, transportId); + c.registerConnection(contactId1, transportId); + c.registerConnection(contactId1, transportId1); + assertEquals(Arrays.asList(contactId, contactId1), + c.getConnectedContacts(transportId)); + assertEquals(Collections.singletonList(contactId1), + c.getConnectedContacts(transportId1)); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java new file mode 100644 index 0000000000000000000000000000000000000000..808f6a8107b68d84b29bf13becb49fc399cf6895 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionWindowTest.java @@ -0,0 +1,157 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.CONNECTION_WINDOW_SIZE; +import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED; +import static org.junit.Assert.assertArrayEquals; + +import java.util.Collection; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class ConnectionWindowTest extends BriarTestCase { + + @Test + public void testWindowSliding() { + ConnectionWindow w = new ConnectionWindow(); + for(int i = 0; i < 100; i++) { + assertFalse(w.isSeen(i)); + w.setSeen(i); + assertTrue(w.isSeen(i)); + } + } + + @Test + public void testWindowJumping() { + ConnectionWindow w = new ConnectionWindow(); + for(int i = 0; i < 100; i += 13) { + assertFalse(w.isSeen(i)); + w.setSeen(i); + assertTrue(w.isSeen(i)); + } + } + + @Test + public void testWindowUpperLimit() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0, highest value in window is 15 + w.setSeen(15); + // Centre is 16, highest value in window is 31 + w.setSeen(31); + try { + // Centre is 32, highest value in window is 47 + w.setSeen(48); + fail(); + } catch(IllegalArgumentException expected) {} + // Centre is max - 1, highest value in window is max + byte[] bitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + w = new ConnectionWindow(MAX_32_BIT_UNSIGNED - 1, bitmap); + assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED - 1)); + assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED)); + // Values greater than max should never be allowed + try { + w.setSeen(MAX_32_BIT_UNSIGNED + 1); + fail(); + } catch(IllegalArgumentException expected) {} + w.setSeen(MAX_32_BIT_UNSIGNED); + assertTrue(w.isSeen(MAX_32_BIT_UNSIGNED)); + // Centre should have moved to max + 1 + assertEquals(MAX_32_BIT_UNSIGNED + 1, w.getCentre()); + // The bit corresponding to max should be set + byte[] expectedBitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + expectedBitmap[expectedBitmap.length / 2 - 1] = 1; // 00000001 + assertArrayEquals(expectedBitmap, w.getBitmap()); + // Values greater than max should never be allowed even if centre > max + try { + w.setSeen(MAX_32_BIT_UNSIGNED + 1); + fail(); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testWindowLowerLimit() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0, negative values should never be allowed + try { + w.setSeen(-1); + fail(); + } catch(IllegalArgumentException expected) {} + // Slide the window + w.setSeen(15); + // Centre is 16, lowest value in window is 0 + w.setSeen(0); + // Slide the window + w.setSeen(16); + // Centre is 17, lowest value in window is 1 + w.setSeen(1); + try { + w.setSeen(0); + fail(); + } catch(IllegalArgumentException expected) {} + // Slide the window + w.setSeen(25); + // Centre is 26, lowest value in window is 10 + w.setSeen(10); + try { + w.setSeen(9); + fail(); + } catch(IllegalArgumentException expected) {} + // Centre should still be 26 + assertEquals(26, w.getCentre()); + // The bits corresponding to 10, 15, 16 and 25 should be set + byte[] expectedBitmap = new byte[CONNECTION_WINDOW_SIZE / 8]; + expectedBitmap[0] = (byte) 134; // 10000110 + expectedBitmap[1] = 1; // 00000001 + assertArrayEquals(expectedBitmap, w.getBitmap()); + } + + @Test + public void testCannotSetSeenTwice() { + ConnectionWindow w = new ConnectionWindow(); + w.setSeen(15); + try { + w.setSeen(15); + fail(); + } catch(IllegalArgumentException expected) {} + } + + @Test + public void testGetUnseenConnectionNumbers() { + ConnectionWindow w = new ConnectionWindow(); + // Centre is 0; window should cover 0 to 15, inclusive, with none seen + Collection<Long> unseen = w.getUnseen(); + assertEquals(16, unseen.size()); + for(int i = 0; i < 16; i++) { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + w.setSeen(3); + w.setSeen(4); + // Centre is 5; window should cover 0 to 20, inclusive, with two seen + unseen = w.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 21; i++) { + if(i == 3 || i == 4) { + assertFalse(unseen.contains(Long.valueOf(i))); + assertTrue(w.isSeen(i)); + } else { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + } + w.setSeen(19); + // Centre is 20; window should cover 4 to 35, inclusive, with two seen + unseen = w.getUnseen(); + assertEquals(30, unseen.size()); + for(int i = 4; i < 36; i++) { + if(i == 4 || i == 19) { + assertFalse(unseen.contains(Long.valueOf(i))); + assertTrue(w.isSeen(i)); + } else { + assertTrue(unseen.contains(Long.valueOf(i))); + assertFalse(w.isSeen(i)); + } + } + } +} diff --git a/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..663700326da5b221ed7f66c5955f7e2b15f2b35c --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/ConnectionWriterImplTest.java @@ -0,0 +1,124 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import net.sf.briar.BriarTestCase; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.Test; + + +public class ConnectionWriterImplTest extends BriarTestCase { + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + @Test + public void testCloseWithoutWritingWritesFinalFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + context.checking(new Expectations() {{ + // Write an empty final frame + oneOf(writer).writeFrame(with(any(byte[].class)), with(0), + with(true)); + // Flush the stream + oneOf(writer).flush(); + }}); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + c.close(); + context.assertIsSatisfied(); + } + + @Test + public void testFlushWithoutBufferedDataWritesFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Flush the stream + oneOf(writer).flush(); + }}); + c.flush(); + context.assertIsSatisfied(); + } + + @Test + public void testFlushWithBufferedDataWritesFrameAndFlushes() + throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write a non-final frame with one payload byte + oneOf(writer).writeFrame(with(any(byte[].class)), with(1), + with(false)); + // Flush the stream + oneOf(writer).flush(); + }}); + c.write(0); + c.flush(); + context.assertIsSatisfied(); + } + + @Test + public void testSingleByteWritesWriteFullFrame() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write a full non-final frame + oneOf(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + }}); + for(int i = 0; i < MAX_PAYLOAD_LENGTH; i++) { + c.write(0); + } + context.assertIsSatisfied(); + } + + @Test + public void testMultiByteWritesWriteFullFrames() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write two full non-final frames + exactly(2).of(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + }}); + // Sanity check + assertEquals(0, MAX_PAYLOAD_LENGTH % 2); + // Write two full payloads using four multi-byte writes + byte[] b = new byte[MAX_PAYLOAD_LENGTH / 2]; + c.write(b); + c.write(b); + c.write(b); + c.write(b); + context.assertIsSatisfied(); + } + + @Test + public void testLargeMultiByteWriteWritesFullFrames() throws Exception { + Mockery context = new Mockery(); + final FrameWriter writer = context.mock(FrameWriter.class); + ConnectionWriterImpl c = new ConnectionWriterImpl(writer, FRAME_LENGTH); + context.checking(new Expectations() {{ + // Write two full non-final frames + exactly(2).of(writer).writeFrame(with(any(byte[].class)), + with(MAX_PAYLOAD_LENGTH), with(false)); + // Write a final frame with a one-byte payload + oneOf(writer).writeFrame(with(any(byte[].class)), with(1), + with(true)); + // Flush the stream + oneOf(writer).flush(); + }}); + // Write two full payloads using one large multi-byte write + byte[] b = new byte[MAX_PAYLOAD_LENGTH * 2 + 1]; + c.write(b); + // There should be one byte left in the buffer + c.close(); + context.assertIsSatisfied(); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..7638bea2c66a4edaa610855e5882439ccdee2057 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java @@ -0,0 +1,183 @@ +package net.sf.briar.transport; + +import static javax.crypto.Cipher.ENCRYPT_MODE; +import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; + +import java.io.ByteArrayInputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.FormatException; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.crypto.CryptoModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class IncomingEncryptionLayerTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + private final CryptoComponent crypto; + private final AuthenticatedCipher frameCipher; + private final ErasableKey frameKey; + + public IncomingEncryptionLayerTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + frameCipher = crypto.getFrameCipher(); + frameKey = crypto.generateTestKey(); + } + + @Test + public void testReadValidFrames() throws Exception { + // Generate two valid frames + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, false, false); + // Concatenate the frames + byte[] valid = new byte[FRAME_LENGTH * 2]; + System.arraycopy(frame, 0, valid, 0, FRAME_LENGTH); + System.arraycopy(frame1, 0, valid, FRAME_LENGTH, FRAME_LENGTH); + // Read the frames + ByteArrayInputStream in = new ByteArrayInputStream(valid); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(123, i.readFrame(buf)); + assertEquals(123, i.readFrame(buf)); + } + + @Test + public void testTruncatedFrameThrowsException() throws Exception { + // Generate a valid frame + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + // Chop off the last byte + byte[] truncated = new byte[FRAME_LENGTH - 1]; + System.arraycopy(frame, 0, truncated, 0, FRAME_LENGTH - 1); + // Try to read the frame, which should fail due to truncation + ByteArrayInputStream in = new ByteArrayInputStream(truncated); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testModifiedFrameThrowsException() throws Exception { + // Generate a valid frame + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false); + // Modify a randomly chosen byte of the frame + frame[(int) (Math.random() * FRAME_LENGTH)] ^= 1; + // Try to read the frame, which should fail due to modification + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testShortNonFinalFrameThrowsException() throws Exception { + // Generate a short non-final frame + byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, false, false); + // Try to read the frame, which should fail due to invalid length + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testShortFinalFrameDoesNotThrowException() throws Exception { + // Generate a short final frame + byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, true, false); + // Read the frame + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + int length = i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + assertEquals(123, length); + } + + @Test + public void testInvalidPayloadLengthThrowsException() throws Exception { + // Generate a frame with an invalid payload length + byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH + 1, + false, false); + // Try to read the frame, which should fail due to invalid length + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testNonZeroPaddingThrowsException() throws Exception { + // Generate a frame with bad padding + byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, true); + // Try to read the frame, which should fail due to bad padding + ByteArrayInputStream in = new ByteArrayInputStream(frame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + try { + i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]); + fail(); + } catch(FormatException expected) {} + } + + @Test + public void testCannotReadBeyondFinalFrame() throws Exception { + // Generate a valid final frame and another valid final frame after it + byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH, true, + false); + byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, true, false); + // Concatenate the frames + byte[] extraFrame = new byte[FRAME_LENGTH * 2]; + System.arraycopy(frame, 0, extraFrame, 0, FRAME_LENGTH); + System.arraycopy(frame1, 0, extraFrame, FRAME_LENGTH, FRAME_LENGTH); + // Read the final frame, which should first read the tag + ByteArrayInputStream in = new ByteArrayInputStream(extraFrame); + IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(buf)); + // The frame after the final frame should not be read + assertEquals(-1, i.readFrame(buf)); + } + + private byte[] generateFrame(long frameNumber, int frameLength, + int payloadLength, boolean finalFrame, boolean badPadding) + throws Exception { + byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH]; + byte[] plaintext = new byte[frameLength - MAC_LENGTH]; + byte[] ciphertext = new byte[frameLength]; + FrameEncoder.encodeIv(iv, frameNumber); + FrameEncoder.encodeAad(aad, frameNumber, plaintext.length); + frameCipher.init(ENCRYPT_MODE, frameKey, iv, aad); + FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength); + if(badPadding) plaintext[HEADER_LENGTH + payloadLength] = 1; + frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0); + return ciphertext; + } +} diff --git a/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..475e5d9adb4d26237e533194994056c6ac86c504 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java @@ -0,0 +1,159 @@ +package net.sf.briar.transport; + +import static javax.crypto.Cipher.ENCRYPT_MODE; +import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; + +import java.io.ByteArrayOutputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.crypto.CryptoModule; + +import org.junit.Test; + +import com.google.inject.Guice; +import com.google.inject.Injector; + +public class OutgoingEncryptionLayerTest extends BriarTestCase { + + // FIXME: This is an integration test, not a unit test + + private static final int FRAME_LENGTH = 1024; + private static final int MAX_PAYLOAD_LENGTH = + FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + + private final CryptoComponent crypto; + private final AuthenticatedCipher frameCipher; + private final byte[] tag; + + public OutgoingEncryptionLayerTest() { + super(); + Injector i = Guice.createInjector(new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + frameCipher = crypto.getFrameCipher(); + tag = new byte[TAG_LENGTH]; + } + + @Test + public void testEncryption() throws Exception { + int payloadLength = 123; + byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH]; + byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH]; + byte[] ciphertext = new byte[FRAME_LENGTH]; + ErasableKey frameKey = crypto.generateTestKey(); + // Calculate the expected ciphertext + FrameEncoder.encodeIv(iv, 0); + FrameEncoder.encodeAad(aad, 0, plaintext.length); + frameCipher.init(ENCRYPT_MODE, frameKey, iv, aad); + FrameEncoder.encodeHeader(plaintext, false, payloadLength); + frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0); + // Check that the actual tag and ciphertext match what's expected + ByteArrayOutputStream out = new ByteArrayOutputStream(); + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, frameKey, FRAME_LENGTH, tag); + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false); + byte[] actual = out.toByteArray(); + assertEquals(TAG_LENGTH + FRAME_LENGTH, actual.length); + for(int i = 0; i < TAG_LENGTH; i++) assertEquals(tag[i], actual[i]); + for(int i = 0; i < FRAME_LENGTH; i++) { + assertEquals("" + i, ciphertext[i], actual[TAG_LENGTH + i]); + } + } + + @Test + public void testInitiatorClosesConnectionWithoutWriting() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Initiator's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH, tag); + // Write an empty final frame without having written any other frames + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true); + // Nothing should be written to the output stream + assertEquals(0, out.size()); + } + + @Test + public void testResponderClosesConnectionWithoutWriting() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Responder's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // Write an empty final frame without having written any other frames + o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true); + // An empty final frame should be written to the output stream + assertEquals(HEADER_LENGTH + MAC_LENGTH, out.size()); + } + + @Test + public void testRemainingCapacityWithTag() throws Exception { + int MAX_PAYLOAD_LENGTH = FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Initiator's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH, tag); + // There should be space for nine full frames and one partial frame + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + assertEquals(10 * MAX_PAYLOAD_LENGTH - TAG_LENGTH, + o.getRemainingCapacity()); + // Write nine frames, each containing a partial payload + for(int i = 0; i < 9; i++) { + o.writeFrame(frame, 123, false); + assertEquals((9 - i) * MAX_PAYLOAD_LENGTH - TAG_LENGTH, + o.getRemainingCapacity()); + } + // Write the final frame, which will not be padded + o.writeFrame(frame, 123, true); + int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH; + assertEquals(MAX_PAYLOAD_LENGTH - TAG_LENGTH - finalFrameLength, + o.getRemainingCapacity()); + } + + @Test + public void testRemainingCapacityWithoutTag() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // Responder's constructor + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + 10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // There should be space for ten full frames + assertEquals(10 * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity()); + // Write nine frames, each containing a partial payload + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + for(int i = 0; i < 9; i++) { + o.writeFrame(frame, 123, false); + assertEquals((9 - i) * MAX_PAYLOAD_LENGTH, + o.getRemainingCapacity()); + } + // Write the final frame, which will not be padded + o.writeFrame(frame, 123, true); + int finalFrameLength = HEADER_LENGTH + 123 + MAC_LENGTH; + assertEquals(MAX_PAYLOAD_LENGTH - finalFrameLength, + o.getRemainingCapacity()); + } + + @Test + public void testRemainingCapacityLimitedByFrameNumbers() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // The connection has plenty of space so we're limited by frame numbers + OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out, + Long.MAX_VALUE, frameCipher, crypto.generateTestKey(), + FRAME_LENGTH); + // There should be enough frame numbers for 2^32 frames + assertEquals((1L << 32) * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity()); + // Write a frame containing a partial payload + byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH]; + o.writeFrame(frame, 123, false); + // There should be enough frame numbers for 2^32 - 1 frames + assertEquals(((1L << 32) - 1) * MAX_PAYLOAD_LENGTH, + o.getRemainingCapacity()); + } +} diff --git a/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java b/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9e8739719f36298aed0509a38530cb26c0489283 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/TransportConnectionRecogniserTest.java @@ -0,0 +1,141 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; +import static org.junit.Assert.assertArrayEquals; + +import java.util.Random; + +import javax.crypto.Cipher; +import javax.crypto.NullCipher; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.api.db.DatabaseComponent; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.TemporarySecret; +import net.sf.briar.util.ByteUtils; + +import org.hamcrest.Description; +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.jmock.api.Action; +import org.jmock.api.Invocation; +import org.junit.Test; + +public class TransportConnectionRecogniserTest extends BriarTestCase { + + private final ContactId contactId = new ContactId(234); + private final TransportId transportId = + new TransportId(TestUtils.getRandomId()); + + @Test + public void testAddAndRemoveSecret() { + Mockery context = new Mockery(); + final CryptoComponent crypto = context.mock(CryptoComponent.class); + final Cipher tagCipher = new NullCipher(); + final byte[] secret = new byte[32]; + new Random().nextBytes(secret); + final boolean alice = false; + final ErasableKey tagKey = context.mock(ErasableKey.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Add secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + // Remove secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + }}); + TemporarySecret s = new TemporarySecret(contactId, transportId, 0L, + 0L, 0L, alice, 0L, secret, 0L, 0L, new byte[4]); + TransportConnectionRecogniser recogniser = + new TransportConnectionRecogniser(crypto, db, transportId); + recogniser.addSecret(s); + recogniser.removeSecret(contactId, 0L); + // The secret should have been erased + assertArrayEquals(new byte[32], secret); + context.assertIsSatisfied(); + } + + @Test + public void testAcceptConnection() throws Exception { + Mockery context = new Mockery(); + final CryptoComponent crypto = context.mock(CryptoComponent.class); + final Cipher tagCipher = new NullCipher(); + final byte[] secret = new byte[32]; + new Random().nextBytes(secret); + final boolean alice = false; + final ErasableKey tagKey = context.mock(ErasableKey.class); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Add secret + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + exactly(16).of(crypto).encodeTag(with(any(byte[].class)), + with(tagCipher), with(tagKey), with(any(long.class))); + will(new EncodeTagAction()); + oneOf(tagKey).erase(); + // Accept connection + oneOf(crypto).getTagCipher(); + will(returnValue(tagCipher)); + oneOf(crypto).deriveTagKey(secret, !alice); + will(returnValue(tagKey)); + // The window should slide to include connection 16 + oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagCipher), + with(tagKey), with(16L)); + will(new EncodeTagAction()); + // The updated window should be stored + oneOf(db).setConnectionWindow(contactId, transportId, 0L, 1L, + new byte[] {0, 1, 0, 0}); + oneOf(tagKey).erase(); + // Accept connection again - no expectations + }}); + TemporarySecret s = new TemporarySecret(contactId, transportId, 0L, + 0L, 0L, alice, 0L, secret, 0L, 0L, new byte[4]); + TransportConnectionRecogniser recogniser = + new TransportConnectionRecogniser(crypto, db, transportId); + recogniser.addSecret(s); + // Connection 0 should be expected + byte[] tag = new byte[TAG_LENGTH]; + ConnectionContext ctx = recogniser.acceptConnection(tag); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(transportId, ctx.getTransportId()); + assertArrayEquals(secret, ctx.getSecret()); + assertEquals(0L, ctx.getConnectionNumber()); + assertEquals(alice, ctx.getAlice()); + context.assertIsSatisfied(); + } + + private static class EncodeTagAction implements Action { + + public void describeTo(Description description) { + description.appendText("Encodes a tag"); + } + + public Object invoke(Invocation invocation) throws Throwable { + byte[] tag = (byte[]) invocation.getParameter(0); + long connection = (Long) invocation.getParameter(3); + ByteUtils.writeUint32(connection, tag, 0); + return null; + } + } +} diff --git a/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..daa99b5b0233d657b1579c0f3b1b66614ed04468 --- /dev/null +++ b/briar-tests/src/net/sf/briar/transport/TransportIntegrationTest.java @@ -0,0 +1,173 @@ +package net.sf.briar.transport; + +import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH; +import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_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.util.Random; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.api.ContactId; +import net.sf.briar.api.crypto.AuthenticatedCipher; +import net.sf.briar.api.crypto.CryptoComponent; +import net.sf.briar.api.crypto.ErasableKey; +import net.sf.briar.api.protocol.TransportId; +import net.sf.briar.api.transport.ConnectionContext; +import net.sf.briar.api.transport.ConnectionReader; +import net.sf.briar.api.transport.ConnectionWriter; +import net.sf.briar.api.transport.ConnectionWriterFactory; +import net.sf.briar.crypto.CryptoModule; +import net.sf.briar.transport.ConnectionReaderImpl; +import net.sf.briar.transport.ConnectionWriterFactoryImpl; +import net.sf.briar.transport.ConnectionWriterImpl; +import net.sf.briar.transport.IncomingEncryptionLayer; +import net.sf.briar.transport.OutgoingEncryptionLayer; + +import org.junit.Test; + + +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; + +public class TransportIntegrationTest extends BriarTestCase { + + private final int FRAME_LENGTH = 2048; + + private final CryptoComponent crypto; + private final ConnectionWriterFactory connectionWriterFactory; + private final ContactId contactId; + private final TransportId transportId; + private final AuthenticatedCipher frameCipher; + private final Random random; + private final byte[] secret; + private final ErasableKey frameKey; + + public TransportIntegrationTest() { + super(); + Module testModule = new AbstractModule() { + @Override + public void configure() { + bind(ConnectionWriterFactory.class).to( + ConnectionWriterFactoryImpl.class); + } + }; + Injector i = Guice.createInjector(testModule, new CryptoModule()); + crypto = i.getInstance(CryptoComponent.class); + connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class); + contactId = new ContactId(234); + transportId = new TransportId(TestUtils.getRandomId()); + frameCipher = crypto.getFrameCipher(); + random = new Random(); + // Since we're sending frames to ourselves, we only need outgoing keys + secret = new byte[32]; + random.nextBytes(secret); + frameKey = crypto.deriveFrameKey(secret, 0L, true, true); + } + + @Test + public void testInitiatorWriteAndRead() throws Exception { + testWriteAndRead(true); + } + + @Test + public void testResponderWriteAndRead() throws Exception { + testWriteAndRead(false); + } + + private void testWriteAndRead(boolean initiator) throws Exception { + // Generate two random frames + byte[] frame = new byte[1234]; + random.nextBytes(frame); + byte[] frame1 = new byte[321]; + random.nextBytes(frame1); + // Copy the frame key - the copy will be erased + ErasableKey frameCopy = frameKey.copy(); + // Write the frames + ByteArrayOutputStream out = new ByteArrayOutputStream(); + FrameWriter encryptionOut = new OutgoingEncryptionLayer(out, + Long.MAX_VALUE, frameCipher, frameCopy, FRAME_LENGTH); + ConnectionWriter writer = new ConnectionWriterImpl(encryptionOut, + FRAME_LENGTH); + OutputStream out1 = writer.getOutputStream(); + out1.write(frame); + out1.flush(); + out1.write(frame1); + out1.flush(); + byte[] output = out.toByteArray(); + assertEquals(FRAME_LENGTH * 2, output.length); + // Read the tag and the frames back + ByteArrayInputStream in = new ByteArrayInputStream(output); + FrameReader encryptionIn = new IncomingEncryptionLayer(in, frameCipher, + frameKey, FRAME_LENGTH); + ConnectionReader reader = new ConnectionReaderImpl(encryptionIn, + FRAME_LENGTH); + InputStream in1 = reader.getInputStream(); + byte[] recovered = new byte[frame.length]; + int offset = 0; + while(offset < recovered.length) { + int read = in1.read(recovered, offset, recovered.length - offset); + if(read == -1) break; + offset += read; + } + assertEquals(recovered.length, offset); + assertArrayEquals(frame, recovered); + byte[] recovered1 = new byte[frame1.length]; + offset = 0; + while(offset < recovered1.length) { + int read = in1.read(recovered1, offset, recovered1.length - offset); + if(read == -1) break; + offset += read; + } + assertEquals(recovered1.length, offset); + assertArrayEquals(frame1, recovered1); + } + + @Test + public void testOverheadWithTag() throws Exception { + ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_CONNECTION_LENGTH); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out, + MIN_CONNECTION_LENGTH, ctx, false, true); + // Check that the connection writer thinks there's room for a packet + long capacity = w.getRemainingCapacity(); + assertTrue(capacity > MAX_PACKET_LENGTH); + assertTrue(capacity < MIN_CONNECTION_LENGTH); + // Check that there really is room for a packet + byte[] payload = new byte[MAX_PACKET_LENGTH]; + w.getOutputStream().write(payload); + w.getOutputStream().close(); + long used = out.size(); + assertTrue(used > MAX_PACKET_LENGTH); + assertTrue(used <= MIN_CONNECTION_LENGTH); + } + + @Test + public void testOverheadWithoutTag() throws Exception { + ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_CONNECTION_LENGTH); + ConnectionContext ctx = new ConnectionContext(contactId, transportId, + secret, 0L, true); + ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out, + MIN_CONNECTION_LENGTH, ctx, false, false); + // Check that the connection writer thinks there's room for a packet + long capacity = w.getRemainingCapacity(); + assertTrue(capacity > MAX_PACKET_LENGTH); + assertTrue(capacity < MIN_CONNECTION_LENGTH); + // Check that there really is room for a packet + byte[] payload = new byte[MAX_PACKET_LENGTH]; + w.getOutputStream().write(payload); + w.getOutputStream().close(); + long used = out.size(); + assertTrue(used > MAX_PACKET_LENGTH); + assertTrue(used <= MIN_CONNECTION_LENGTH); + } +} diff --git a/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java b/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c672bc0e2af7e72bbcce2171f27a72952cbfa775 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/ByteUtilsTest.java @@ -0,0 +1,66 @@ +package net.sf.briar.util; + +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class ByteUtilsTest extends BriarTestCase { + + @Test + public void testReadUint16() { + byte[] b = StringUtils.fromHexString("000000"); + assertEquals(0, ByteUtils.readUint16(b, 1)); + b = StringUtils.fromHexString("000001"); + assertEquals(1, ByteUtils.readUint16(b, 1)); + b = StringUtils.fromHexString("00FFFF"); + assertEquals(65535, ByteUtils.readUint16(b, 1)); + } + + @Test + public void testReadUint32() { + byte[] b = StringUtils.fromHexString("0000000000"); + assertEquals(0L, ByteUtils.readUint32(b, 1)); + b = StringUtils.fromHexString("0000000001"); + assertEquals(1L, ByteUtils.readUint32(b, 1)); + b = StringUtils.fromHexString("00FFFFFFFF"); + assertEquals(4294967295L, ByteUtils.readUint32(b, 1)); + } + + + @Test + public void testWriteUint16() { + byte[] b = new byte[3]; + ByteUtils.writeUint16(0, b, 1); + assertEquals("000000", StringUtils.toHexString(b)); + ByteUtils.writeUint16(1, b, 1); + assertEquals("000001", StringUtils.toHexString(b)); + ByteUtils.writeUint16(65535, b, 1); + assertEquals("00FFFF", StringUtils.toHexString(b)); + } + + @Test + public void testWriteUint32() { + byte[] b = new byte[5]; + ByteUtils.writeUint32(0L, b, 1); + assertEquals("0000000000", StringUtils.toHexString(b)); + ByteUtils.writeUint32(1L, b, 1); + assertEquals("0000000001", StringUtils.toHexString(b)); + ByteUtils.writeUint32(4294967295L, b, 1); + assertEquals("00FFFFFFFF", StringUtils.toHexString(b)); + } + + @Test + public void testReadUint() { + byte[] b = new byte[1]; + b[0] = (byte) 128; + for(int i = 0; i < 8; i++) { + assertEquals(1 << i, ByteUtils.readUint(b, i + 1)); + } + b = new byte[2]; + for(int i = 0; i < 65535; i++) { + ByteUtils.writeUint16(i, b, 0); + assertEquals(i, ByteUtils.readUint(b, 16)); + assertEquals(i >> 1, ByteUtils.readUint(b, 15)); + } + } +} diff --git a/briar-tests/src/net/sf/briar/util/FileUtilsTest.java b/briar-tests/src/net/sf/briar/util/FileUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..251d78a6e8aec8ce28c5779972237dac8832ab80 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/FileUtilsTest.java @@ -0,0 +1,165 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.util.FileUtils.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FileUtilsTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testCreateTempFile() throws IOException { + File temp = FileUtils.createTempFile(); + assertTrue(temp.exists()); + assertTrue(temp.isFile()); + assertEquals(0L, temp.length()); + temp.delete(); + } + + @Test + public void testCopy() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n"); + long length = src.length(); + + FileUtils.copy(src, dest); + + assertEquals(length, dest.length()); + Scanner in = new Scanner(dest); + assertTrue(in.hasNextLine()); + assertEquals("Foo bar", in.nextLine()); + assertTrue(in.hasNextLine()); + assertEquals("Bar foo", in.nextLine()); + assertFalse(in.hasNext()); + in.close(); + } + + @Test + public void testCopyFromStream() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "Foo bar\r\nBar foo\r\n"); + long length = src.length(); + InputStream is = new FileInputStream(src); + is.skip(4); + + FileUtils.copy(is, dest); + + assertEquals(length - 4, dest.length()); + Scanner in = new Scanner(dest); + assertTrue(in.hasNextLine()); + assertEquals("bar", in.nextLine()); + assertTrue(in.hasNextLine()); + assertEquals("Bar foo", in.nextLine()); + assertFalse(in.hasNext()); + in.close(); + } + + @Test + public void testCopyRecursively() throws IOException { + final File dest1 = new File(testDir, "dest/abc/def/1"); + final File dest2 = new File(testDir, "dest/abc/def/2"); + final File dest3 = new File(testDir, "dest/abc/3"); + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(dest1); + oneOf(callback).processingFile(dest2); + oneOf(callback).processingFile(dest3); + }}); + + copyRecursively(callback); + + context.assertIsSatisfied(); + } + + @Test + public void testCopyRecursivelyNoCallback() throws IOException { + copyRecursively(null); + } + + private void copyRecursively(Callback callback) throws IOException { + TestUtils.createFile(new File(testDir, "abc/def/1"), "one one one"); + TestUtils.createFile(new File(testDir, "abc/def/2"), "two two two"); + TestUtils.createFile(new File(testDir, "abc/3"), "three three three"); + + File dest = new File(testDir, "dest"); + dest.mkdir(); + + FileUtils.copyRecursively(new File(testDir, "abc"), dest, callback); + + File dest1 = new File(testDir, "dest/abc/def/1"); + assertTrue(dest1.exists()); + assertTrue(dest1.isFile()); + assertEquals("one one one".length(), dest1.length()); + File dest2 = new File(testDir, "dest/abc/def/2"); + assertTrue(dest2.exists()); + assertTrue(dest2.isFile()); + assertEquals("two two two".length(), dest2.length()); + File dest3 = new File(testDir, "dest/abc/3"); + assertTrue(dest3.exists()); + assertTrue(dest3.isFile()); + assertEquals("three three three".length(), dest3.length()); + } + + @Test + public void testDeleteFile() throws IOException { + File foo = new File(testDir, "foo"); + foo.createNewFile(); + assertTrue(foo.exists()); + + FileUtils.delete(foo); + + assertFalse(foo.exists()); + } + + @Test + public void testDeleteDirectory() throws IOException { + File f1 = new File(testDir, "abc/def/1"); + File f2 = new File(testDir, "abc/def/2"); + File f3 = new File(testDir, "abc/3"); + File abc = new File(testDir, "abc"); + File def = new File(testDir, "abc/def"); + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + + assertTrue(f1.exists()); + assertTrue(f2.exists()); + assertTrue(f3.exists()); + assertTrue(abc.exists()); + assertTrue(def.exists()); + + FileUtils.delete(def); + + assertFalse(f1.exists()); + assertFalse(f2.exists()); + assertTrue(f3.exists()); + assertTrue(abc.exists()); + assertFalse(def.exists()); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +} diff --git a/briar-tests/src/net/sf/briar/util/StringUtilsTest.java b/briar-tests/src/net/sf/briar/util/StringUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d4465e062e688152ccae97c2cc9205085b7d19ce --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/StringUtilsTest.java @@ -0,0 +1,44 @@ +package net.sf.briar.util; + +import static org.junit.Assert.assertArrayEquals; +import net.sf.briar.BriarTestCase; + +import org.junit.Test; + +public class StringUtilsTest extends BriarTestCase { + + @Test + public void testHead() { + String head = StringUtils.head("123456789", 5); + assertEquals("12345...", head); + } + + @Test + public void testTail() { + String tail = StringUtils.tail("987654321", 5); + assertEquals("...54321", tail); + } + + @Test + public void testToHexString() { + byte[] b = new byte[] {1, 2, 3, 127, -128}; + String s = StringUtils.toHexString(b); + assertEquals("0102037F80", s); + } + + @Test + public void testFromHexString() { + try { + StringUtils.fromHexString("12345"); + fail(); + } catch(IllegalArgumentException expected) {} + try { + StringUtils.fromHexString("ABCDEFGH"); + fail(); + } catch(IllegalArgumentException expected) {} + byte[] b = StringUtils.fromHexString("0102037F80"); + assertArrayEquals(new byte[] {1, 2, 3, 127, -128}, b); + b = StringUtils.fromHexString("0a0b0c0d0e0f"); + assertArrayEquals(new byte[] {10, 11, 12, 13, 14, 15}, b); + } +} diff --git a/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java b/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..c08d4a3700f407c5fe55a9e4ec4285f0752bd0b5 --- /dev/null +++ b/briar-tests/src/net/sf/briar/util/ZipUtilsTest.java @@ -0,0 +1,202 @@ +package net.sf.briar.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Scanner; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import net.sf.briar.BriarTestCase; +import net.sf.briar.TestUtils; +import net.sf.briar.util.ZipUtils.Callback; + +import org.jmock.Expectations; +import org.jmock.Mockery; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class ZipUtilsTest extends BriarTestCase { + + private final File testDir = TestUtils.getTestDirectory(); + + private final File f1 = new File(testDir, "abc/def/1"); + private final File f2 = new File(testDir, "abc/def/2"); + private final File f3 = new File(testDir, "abc/3"); + + @Before + public void setUp() { + testDir.mkdirs(); + } + + @Test + public void testCopyToZip() throws IOException { + File src = new File(testDir, "src"); + File dest = new File(testDir, "dest"); + TestUtils.createFile(src, "foo bar baz"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + + ZipUtils.copyToZip("abc/def", src, zip); + zip.flush(); + zip.close(); + + Map<String, String> expected = Collections.singletonMap("abc/def", + "foo bar baz"); + checkZipEntries(dest, expected); + } + + private void checkZipEntries(File f, Map<String, String> expected) + throws IOException { + Map<String, String> found = new HashMap<String, String>(); + assertTrue(f.exists()); + assertTrue(f.isFile()); + ZipInputStream unzip = new ZipInputStream(new FileInputStream(f)); + ZipEntry entry; + while((entry = unzip.getNextEntry()) != null) { + String name = entry.getName(); + Scanner s = new Scanner(unzip); + assertTrue(s.hasNextLine()); + String contents = s.nextLine(); + assertFalse(s.hasNextLine()); + unzip.closeEntry(); + found.put(name, contents); + } + unzip.close(); + assertEquals(expected.size(), found.size()); + for(String name : expected.keySet()) { + String contents = found.get(name); + assertNotNull(contents); + assertEquals(expected.get(name), contents); + } + } + + @Test + public void testCopyToZipRecursively() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + oneOf(callback).processingFile(f3); + }}); + + copyRecursively(callback); + + context.assertIsSatisfied(); + } + + @Test + public void testCopyToZipRecursivelyNoCallback() throws IOException { + copyRecursively(null); + } + + private void copyRecursively(Callback callback) throws IOException { + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + File src = new File(testDir, "abc"); + File dest = new File(testDir, "dest"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + + ZipUtils.copyToZipRecursively("ghi", src, zip, callback); + zip.flush(); + zip.close(); + + Map<String, String> expected = new HashMap<String, String>(); + expected.put("ghi/def/1", "one one one"); + expected.put("ghi/def/2", "two two two"); + expected.put("ghi/3", "three three three"); + checkZipEntries(dest, expected); + } + + @Test + public void testUnzipStream() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + oneOf(callback).processingFile(f3); + }}); + + unzipStream(null, callback); + + context.assertIsSatisfied(); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertTrue(f3.exists()); + assertTrue(f3.isFile()); + assertEquals("three three three".length(), f3.length()); + } + + @Test + public void testUnzipStreamWithRegex() throws IOException { + Mockery context = new Mockery(); + final Callback callback = context.mock(Callback.class); + context.checking(new Expectations() {{ + oneOf(callback).processingFile(f1); + oneOf(callback).processingFile(f2); + }}); + + unzipStream("^abc/def/.*", callback); + + context.assertIsSatisfied(); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertFalse(f3.exists()); + } + + @Test + public void testUnzipStreamNoCallback() throws IOException { + unzipStream(null, null); + + assertTrue(f1.exists()); + assertTrue(f1.isFile()); + assertEquals("one one one".length(), f1.length()); + assertTrue(f2.exists()); + assertTrue(f2.isFile()); + assertEquals("two two two".length(), f2.length()); + assertTrue(f3.exists()); + assertTrue(f3.isFile()); + assertEquals("three three three".length(), f3.length()); + } + + private void unzipStream(String regex, Callback callback) + throws IOException { + TestUtils.createFile(f1, "one one one"); + TestUtils.createFile(f2, "two two two"); + TestUtils.createFile(f3, "three three three"); + File src = new File(testDir, "abc"); + File dest = new File(testDir, "dest"); + ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(dest)); + ZipUtils.copyToZipRecursively(src.getName(), src, zip, null); + zip.flush(); + zip.close(); + TestUtils.delete(src); + + InputStream in = new FileInputStream(dest); + ZipUtils.unzipStream(in, testDir, regex, callback); + } + + @After + public void tearDown() { + TestUtils.deleteTestDirectory(testDir); + } +}