diff --git a/api/net/sf/briar/api/protocol/AuthorId.java b/api/net/sf/briar/api/protocol/AuthorId.java
index 8b32851a8b766e6e1198e1466decb730393c9444..578c89c2accf4374de24d59bfcd7541808d79f24 100644
--- a/api/net/sf/briar/api/protocol/AuthorId.java
+++ b/api/net/sf/briar/api/protocol/AuthorId.java
@@ -3,26 +3,10 @@ package net.sf.briar.api.protocol;
 import java.util.Arrays;
 
 /** Type-safe wrapper for a byte array that uniquely identifies an author. */
-public class AuthorId {
-
-	public static final int LENGTH = 32;
-
-	// FIXME: Replace this with an isSelf() method that compares an AuthorId
-	// to any and all local AuthorIds.
-	public static final AuthorId SELF = new AuthorId(new byte[] {
-			0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
-			16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
-	});
-
-	private final byte[] id;
+public class AuthorId extends UniqueId {
 
 	public AuthorId(byte[] id) {
-		assert id.length == LENGTH;
-		this.id = id;
-	}
-
-	public byte[] getBytes() {
-		return id;
+		super(id);
 	}
 
 	@Override
@@ -31,9 +15,4 @@ public class AuthorId {
 			return Arrays.equals(id, ((AuthorId) o).id);
 		return false;
 	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(id);
-	}
 }
diff --git a/api/net/sf/briar/api/protocol/BatchId.java b/api/net/sf/briar/api/protocol/BatchId.java
index 4b4578b03f32ace399bd6482e9206ce861cbe8f3..ab33800761d9558416896dde3862f3c69d83793e 100644
--- a/api/net/sf/briar/api/protocol/BatchId.java
+++ b/api/net/sf/briar/api/protocol/BatchId.java
@@ -6,19 +6,10 @@ import java.util.Arrays;
  * Type-safe wrapper for a byte array that uniquely identifies a batch of
  * messages.
  */
-public class BatchId {
-
-	public static final int LENGTH = 32;
-
-	private final byte[] id;
+public class BatchId extends UniqueId {
 
 	public BatchId(byte[] id) {
-		assert id.length == LENGTH;
-		this.id = id;
-	}
-
-	public byte[] getBytes() {
-		return id;
+		super(id);
 	}
 
 	@Override
@@ -27,9 +18,4 @@ public class BatchId {
 			return Arrays.equals(id, ((BatchId) o).id);
 		return false;
 	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(id);
-	}
 }
diff --git a/api/net/sf/briar/api/protocol/BundleId.java b/api/net/sf/briar/api/protocol/BundleId.java
index 2dd7e8f59f5483fa68cf0bc470a34eb429010b04..dfc9f285a5f326c854a7e2a50448d019f7619cf6 100644
--- a/api/net/sf/briar/api/protocol/BundleId.java
+++ b/api/net/sf/briar/api/protocol/BundleId.java
@@ -6,24 +6,15 @@ import java.util.Arrays;
  * Type-safe wrapper for a byte array that uniquely identifies a bundle of
  * acknowledgements, subscriptions, and batches of messages.
  */
-public class BundleId {
+public class BundleId extends UniqueId {
 
 	public static final BundleId NONE = new BundleId(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
 	});
 
-	public static final int LENGTH = 32;
-
-	private final byte[] id;
-
 	public BundleId(byte[] id) {
-		assert id.length == LENGTH;
-		this.id = id;
-	}
-
-	public byte[] getBytes() {
-		return id;
+		super(id);
 	}
 
 	@Override
@@ -32,9 +23,4 @@ public class BundleId {
 			return Arrays.equals(id, ((BundleId) o).id);
 		return false;
 	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(id);
-	}
 }
diff --git a/api/net/sf/briar/api/protocol/GroupId.java b/api/net/sf/briar/api/protocol/GroupId.java
index 46c0456970c737cfaf5d057fe752f012750f4f86..532941867942b4bb02f4e8a6ed828820c7cbb20f 100644
--- a/api/net/sf/briar/api/protocol/GroupId.java
+++ b/api/net/sf/briar/api/protocol/GroupId.java
@@ -6,19 +6,10 @@ import java.util.Arrays;
  * Type-safe wrapper for a byte array that uniquely identifies a group to which
  * users may subscribe.
  */
-public class GroupId {
-
-	public static final int LENGTH = 32;
-
-	private final byte[] id;
+public class GroupId extends UniqueId {
 
 	public GroupId(byte[] id) {
-		assert id.length == LENGTH;
-		this.id = id;
-	}
-
-	public byte[] getBytes() {
-		return id;
+		super(id);
 	}
 
 	@Override
@@ -27,9 +18,4 @@ public class GroupId {
 			return Arrays.equals(id, ((GroupId) o).id);
 		return false;
 	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(id);
-	}
 }
diff --git a/api/net/sf/briar/api/protocol/MessageId.java b/api/net/sf/briar/api/protocol/MessageId.java
index ee25caddc0f9a4e2b9b107e1b3c91859b6e0a350..64e0fd0369ce2a47a226078bc115e455990dcce6 100644
--- a/api/net/sf/briar/api/protocol/MessageId.java
+++ b/api/net/sf/briar/api/protocol/MessageId.java
@@ -3,7 +3,7 @@ package net.sf.briar.api.protocol;
 import java.util.Arrays;
 
 /** Type-safe wrapper for a byte array that uniquely identifies a message. */
-public class MessageId {
+public class MessageId extends UniqueId {
 
 	/** Used to indicate that the first message in a thread has no parent. */
 	public static final MessageId NONE = new MessageId(new byte[] {
@@ -11,17 +11,8 @@ public class MessageId {
 			0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
 	});
 
-	public static final int LENGTH = 32;
-
-	private final byte[] id;
-
 	public MessageId(byte[] id) {
-		assert id.length == LENGTH;
-		this.id = id;
-	}
-
-	public byte[] getBytes() {
-		return id;
+		super(id);
 	}
 
 	@Override
@@ -30,9 +21,4 @@ public class MessageId {
 			return Arrays.equals(id, ((MessageId) o).id);
 		return false;
 	}
-
-	@Override
-	public int hashCode() {
-		return Arrays.hashCode(id);
-	}
 }
diff --git a/api/net/sf/briar/api/protocol/UniqueId.java b/api/net/sf/briar/api/protocol/UniqueId.java
new file mode 100644
index 0000000000000000000000000000000000000000..3eb9cdd15d8eb4d9beaaf620d6a903a699c62c91
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/UniqueId.java
@@ -0,0 +1,24 @@
+package net.sf.briar.api.protocol;
+
+import java.util.Arrays;
+
+public abstract class UniqueId {
+
+	public static final int LENGTH = 32;
+
+	protected final byte[] id;
+
+	protected UniqueId(byte[] id) {
+		assert id.length == LENGTH;
+		this.id = id;
+	}
+
+	public byte[] getBytes() {
+		return id;
+	}
+
+	@Override
+	public int hashCode() {
+		return Arrays.hashCode(id);
+	}
+}
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index 82bd7156e1550ae011d6ca9dfd36b86842255b54..edd4fe55661c895eb4fac16f12f51a7e30870f71 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -61,7 +61,7 @@ DatabaseCleaner.Callback {
 	private int calculateSendability(Txn txn, Message m) throws DbException {
 		int sendability = 0;
 		// One point for a good rating
-		if(getRating(m.getAuthor()) == Rating.GOOD) sendability++;
+		if(db.getRating(txn, m.getAuthor()) == Rating.GOOD) sendability++;
 		// One point per sendable child (backward inclusion)
 		sendability += db.getNumberOfSendableChildren(txn, m.getId());
 		return sendability;
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..30054c911359ee79a363cb34d80396bdaff565e5
--- /dev/null
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -0,0 +1,519 @@
+package net.sf.briar.db;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Random;
+import java.util.Set;
+
+import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.db.ContactId;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.Rating;
+import net.sf.briar.api.db.Status;
+import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.Batch;
+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.protocol.MessageImpl;
+
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Provider;
+
+public abstract class DatabaseComponentTest extends TestCase {
+
+	private final File testDir = TestUtils.getTestDirectory();
+	private final Random random = new Random();
+	private final AuthorId authorId;
+	private final ContactId contactId;
+	private final GroupId groupId;
+	private final MessageId messageId, parentId;
+	private final long timestamp;
+	private final int size;
+	private final byte[] body;
+	private final Message message;
+	private final Object txn = new Object();
+
+	public DatabaseComponentTest() {
+		super();
+		authorId = new AuthorId(getRandomId());
+		contactId = new ContactId(123);
+		groupId = new GroupId(getRandomId());
+		messageId = new MessageId(getRandomId());
+		parentId = new MessageId(getRandomId());
+		timestamp = System.currentTimeMillis();
+		size = 1234;
+		body = new byte[size];
+		random.nextBytes(body);
+		message = new MessageImpl(messageId, MessageId.NONE, groupId, authorId,
+				timestamp, body);
+	}
+
+	protected abstract <T> DatabaseComponent createDatabaseComponent(
+			Database<T> database, DatabaseCleaner cleaner,
+			Provider<Batch> batchProvider);
+
+	@Before
+	public void setUp() {
+		testDir.mkdirs();
+	}
+
+	@Test
+	public void testSimpleCalls() throws DbException {
+		final Set<GroupId> subs = Collections.singleton(groupId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// getRating(authorId)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).getRating(txn, authorId);
+			will(returnValue(Rating.UNRATED));
+			oneOf(database).commitTransaction(txn);
+			// addContact(contactId)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).addContact(txn, contactId);
+			oneOf(database).commitTransaction(txn);
+			// subscribe(groupId)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).addSubscription(txn, groupId);
+			oneOf(database).commitTransaction(txn);
+			// getSubscriptions()
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).getSubscriptions(txn);
+			will(returnValue(subs));
+			oneOf(database).commitTransaction(txn);
+			// unsubscribe(groupId)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).removeSubscription(txn, groupId);
+			oneOf(database).commitTransaction(txn);
+			// removeContact(contactId)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).removeContact(txn, contactId);
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		assertEquals(Rating.UNRATED, db.getRating(authorId));
+		db.addContact(contactId);
+		db.subscribe(groupId);
+		assertEquals(Collections.singleton(groupId), db.getSubscriptions());
+		db.unsubscribe(groupId);
+		db.removeContact(contactId);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testNoParentStopsBackwardInclusion() throws DbException {
+		final Set<MessageId> messages = Collections.singleton(messageId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// setRating(Rating.GOOD)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).setRating(txn, authorId, Rating.GOOD);
+			// The sendability of the author's messages should be incremented
+			oneOf(database).getMessagesByAuthor(txn, authorId);
+			will(returnValue(messages));
+			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).getParent(txn, messageId);
+			will(returnValue(MessageId.NONE));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.setRating(authorId, Rating.GOOD);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testMissingParentStopsBackwardInclusion() throws DbException {
+		final Set<MessageId> messages = Collections.singleton(messageId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// setRating(Rating.GOOD)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).setRating(txn, authorId, Rating.GOOD);
+			// The sendability of the author's messages should be incremented
+			oneOf(database).getMessagesByAuthor(txn, authorId);
+			will(returnValue(messages));
+			oneOf(database).getSendability(txn, messageId);
+			will(returnValue(0));
+			oneOf(database).setSendability(txn, messageId, 1);
+			// The parent exists
+			oneOf(database).getParent(txn, messageId);
+			will(returnValue(parentId));
+			// The parent isn't in the DB
+			oneOf(database).containsMessage(txn, parentId);
+			will(returnValue(false));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.setRating(authorId, Rating.GOOD);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testChangingGroupsStopsBackwardInclusion() throws DbException {
+		final GroupId groupId1 = new GroupId(getRandomId());
+		final Set<MessageId> messages = Collections.singleton(messageId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// setRating(Rating.GOOD)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).setRating(txn, authorId, Rating.GOOD);
+			// The sendability of the author's messages should be incremented
+			oneOf(database).getMessagesByAuthor(txn, authorId);
+			will(returnValue(messages));
+			oneOf(database).getSendability(txn, messageId);
+			will(returnValue(0));
+			oneOf(database).setSendability(txn, messageId, 1);
+			// The parent exists and is in the database
+			oneOf(database).getParent(txn, messageId);
+			will(returnValue(parentId));
+			oneOf(database).containsMessage(txn, parentId);
+			will(returnValue(true));
+			// The parent is in a different group
+			oneOf(database).getGroup(txn, messageId);
+			will(returnValue(groupId));
+			oneOf(database).getGroup(txn, parentId);
+			will(returnValue(groupId1));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.setRating(authorId, Rating.GOOD);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testUnaffectedParentStopsBackwardInclusion()
+	throws DbException {
+		final Set<MessageId> messages = Collections.singleton(messageId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// setRating(Rating.GOOD)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).setRating(txn, authorId, Rating.GOOD);
+			// The sendability of the author's messages should be incremented
+			oneOf(database).getMessagesByAuthor(txn, authorId);
+			will(returnValue(messages));
+			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).getParent(txn, messageId);
+			will(returnValue(parentId));
+			oneOf(database).containsMessage(txn, parentId);
+			will(returnValue(true));
+			oneOf(database).getGroup(txn, messageId);
+			will(returnValue(groupId));
+			oneOf(database).getGroup(txn, parentId);
+			will(returnValue(groupId));
+			// The parent is already sendable
+			oneOf(database).getSendability(txn, parentId);
+			will(returnValue(1));
+			oneOf(database).setSendability(txn, parentId, 2);
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.setRating(authorId, Rating.GOOD);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testAffectedParentContinuesBackwardInclusion()
+	throws DbException {
+		final Set<MessageId> messages = Collections.singleton(messageId);
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// setRating(Rating.GOOD)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).setRating(txn, authorId, Rating.GOOD);
+			// The sendability of the author's messages should be incremented
+			oneOf(database).getMessagesByAuthor(txn, authorId);
+			will(returnValue(messages));
+			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).getParent(txn, messageId);
+			will(returnValue(parentId));
+			oneOf(database).containsMessage(txn, parentId);
+			will(returnValue(true));
+			oneOf(database).getGroup(txn, messageId);
+			will(returnValue(groupId));
+			oneOf(database).getGroup(txn, parentId);
+			will(returnValue(groupId));
+			// The parent is not already sendable
+			oneOf(database).getSendability(txn, parentId);
+			will(returnValue(0));
+			oneOf(database).setSendability(txn, parentId, 1);
+			oneOf(database).getParent(txn, parentId);
+			will(returnValue(MessageId.NONE));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.setRating(authorId, Rating.GOOD);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testMessagesAreNotStoredUnlessSubscribed()
+	throws DbException {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// addLocallyGeneratedMessage(message)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsSubscription(txn, groupId);
+			will(returnValue(false));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.addLocallyGeneratedMessage(message);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testDuplicateMessagesAreNotStored() throws DbException {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// addLocallyGeneratedMessage(message)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsSubscription(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).addMessage(txn, message);
+			will(returnValue(false));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.addLocallyGeneratedMessage(message);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testAddLocallyGeneratedMessage() throws DbException {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// addLocallyGeneratedMessage(message)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsSubscription(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).addMessage(txn, message);
+			will(returnValue(true));
+			oneOf(database).getContacts(txn);
+			will(returnValue(Collections.singleton(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);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.addLocallyGeneratedMessage(message);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testAddingASendableMessageTriggersBackwardInclusion()
+	throws DbException {
+		Mockery context = new Mockery();
+		@SuppressWarnings("unchecked")
+		final Database<Object> database = context.mock(Database.class);
+		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
+		@SuppressWarnings("unchecked")
+		final Provider<Batch> batchProvider = context.mock(Provider.class);
+		context.checking(new Expectations() {{
+			oneOf(database).open(false);
+			oneOf(cleaner).startCleaning();
+			// addLocallyGeneratedMessage(message)
+			oneOf(database).startTransaction();
+			will(returnValue(txn));
+			oneOf(database).containsSubscription(txn, groupId);
+			will(returnValue(true));
+			oneOf(database).addMessage(txn, message);
+			will(returnValue(true));
+			oneOf(database).getContacts(txn);
+			will(returnValue(Collections.singleton(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).getParent(txn, messageId);
+			will(returnValue(MessageId.NONE));
+			oneOf(database).commitTransaction(txn);
+			oneOf(cleaner).stopCleaning();
+			oneOf(database).close();
+		}});
+		DatabaseComponent db = createDatabaseComponent(database, cleaner,
+				batchProvider);
+
+		db.open(false);
+		db.addLocallyGeneratedMessage(message);
+		db.close();
+
+		context.assertIsSatisfied();
+	}
+
+	private byte[] getRandomId() {
+		byte[] b = new byte[32];
+		random.nextBytes(b);
+		return b;
+	}
+
+	@After
+	public void tearDown() {
+		TestUtils.deleteTestDirectory(testDir);
+	}
+}
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 7b87d0b84b5cad257ad6e7d50ddced3e7228f976..483b507af2c0dae8b13848c00d44626e109b945d 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -40,7 +40,7 @@ public class H2DatabaseTest extends TestCase {
 	private final File testDir = TestUtils.getTestDirectory();
 	// The password has the format <file password> <space> <user password>
 	private final String passwordString = "foo bar";
-	private final Random random;
+	private final Random random = new Random();
 	private final AuthorId authorId;
 	private final BatchId batchId;
 	private final ContactId contactId;
@@ -53,7 +53,6 @@ public class H2DatabaseTest extends TestCase {
 
 	public H2DatabaseTest() {
 		super();
-		random = new Random();
 		authorId = new AuthorId(getRandomId());
 		batchId = new BatchId(getRandomId());
 		contactId = new ContactId(123);
diff --git a/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8de0dc1802bd98ed242a6f33396eaccf28fd76ef
--- /dev/null
+++ b/test/net/sf/briar/db/ReadWriteLockDatabaseComponentTest.java
@@ -0,0 +1,17 @@
+package net.sf.briar.db;
+
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.protocol.Batch;
+
+import com.google.inject.Provider;
+
+public class ReadWriteLockDatabaseComponentTest extends DatabaseComponentTest {
+
+	@Override
+	protected <T> DatabaseComponent createDatabaseComponent(
+			Database<T> database, DatabaseCleaner cleaner,
+			Provider<Batch> batchProvider) {
+		return new ReadWriteLockDatabaseComponent<T>(database, cleaner,
+				batchProvider);
+	}
+}
diff --git a/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..758ce1ce4815fb106d83b6dab454db08448487a3
--- /dev/null
+++ b/test/net/sf/briar/db/SynchronizedDatabaseComponentTest.java
@@ -0,0 +1,18 @@
+package net.sf.briar.db;
+
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.protocol.Batch;
+
+import com.google.inject.Provider;
+
+public class SynchronizedDatabaseComponentTest extends DatabaseComponentTest {
+
+	@Override
+	protected <T> DatabaseComponent createDatabaseComponent(
+			Database<T> database, DatabaseCleaner cleaner,
+			Provider<Batch> batchProvider) {
+		return new SynchronizedDatabaseComponent<T>(database, cleaner,
+				batchProvider);
+	}
+
+}