diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 85f383bae4372b7cfdad2e91515b4e1df94e6a1e..248602434ef9cadf46cab96105777a4630c13dad 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -9,6 +9,7 @@ import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.db.NoSuchMessageException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
@@ -23,8 +24,10 @@ import org.briarproject.api.sync.MessageValidator;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.util.ByteUtils;
 
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
@@ -81,19 +84,48 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					// TODO: Don't do all of this in a single DB task
+					Queue<MessageId> unvalidated = new LinkedList<MessageId>();
 					Transaction txn = db.startTransaction();
 					try {
-						for (MessageId id : db.getMessagesToValidate(txn, c)) {
-							byte[] raw = db.getRawMessage(txn, id);
-							Message m = parseMessage(id, raw);
-							Group g = db.getGroup(txn, m.getGroupId());
-							validateMessage(m, g);
-						}
+						unvalidated.addAll(db.getMessagesToValidate(txn, c));
+						txn.setComplete();
+					} finally {
+						db.endTransaction(txn);
+					}
+					validateNextMessage(unvalidated);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	private void validateNextMessage(final Queue<MessageId> unvalidated) {
+		if (unvalidated.isEmpty()) return;
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					Message m = null;
+					Group g = null;
+					Transaction txn = db.startTransaction();
+					try {
+						MessageId id = unvalidated.poll();
+						byte[] raw = db.getRawMessage(txn, id);
+						m = parseMessage(id, raw);
+						g = db.getGroup(txn, m.getGroupId());
 						txn.setComplete();
+					} catch (NoSuchMessageException e) {
+						LOG.info("Message removed before validation");
+						// Continue to next message
+					} catch (NoSuchGroupException e) {
+						LOG.info("Group removed before validation");
+						// Continue to next message
 					} finally {
 						db.endTransaction(txn);
 					}
+					if (m != null && g != null) validateMessage(m, g);
+					validateNextMessage(unvalidated);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
@@ -158,21 +190,23 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		if (e instanceof MessageAddedEvent) {
 			// Validate the message if it wasn't created locally
 			MessageAddedEvent m = (MessageAddedEvent) e;
-			if (m.getContactId() != null) loadGroup(m.getMessage());
+			if (m.getContactId() != null) loadGroupAndValidate(m.getMessage());
 		}
 	}
 
-	private void loadGroup(final Message m) {
+	private void loadGroupAndValidate(final Message m) {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
+					Group g;
 					Transaction txn = db.startTransaction();
 					try {
-						validateMessage(m, db.getGroup(txn, m.getGroupId()));
+						g = db.getGroup(txn, m.getGroupId());
 						txn.setComplete();
 					} finally {
 						db.endTransaction(txn);
 					}
+					validateMessage(m, g);
 				} catch (NoSuchGroupException e) {
 					LOG.info("Group removed before validation");
 				} catch (DbException e) {
diff --git a/briar-tests/src/org/briarproject/plugins/ImmediateExecutor.java b/briar-tests/src/org/briarproject/ImmediateExecutor.java
similarity index 81%
rename from briar-tests/src/org/briarproject/plugins/ImmediateExecutor.java
rename to briar-tests/src/org/briarproject/ImmediateExecutor.java
index bac8cf1e5df75120fb1ceb1e9eefa2b1a70e314b..ce0267885452e75a4e7fcac39e729248529c269d 100644
--- a/briar-tests/src/org/briarproject/plugins/ImmediateExecutor.java
+++ b/briar-tests/src/org/briarproject/ImmediateExecutor.java
@@ -1,4 +1,4 @@
-package org.briarproject.plugins;
+package org.briarproject;
 
 import java.util.concurrent.Executor;
 
diff --git a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
index a70d0eaff0d9eeca8ba54a5cd5252f210167e648..cd6d4c2f892fb2587d700e8f70b4f51a79094972 100644
--- a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
@@ -1,11 +1,11 @@
 package org.briarproject.plugins.file;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.ImmediateExecutor;
 import org.briarproject.TestUtils;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
-import org.briarproject.plugins.ImmediateExecutor;
 import org.briarproject.plugins.file.RemovableDriveMonitor.Callback;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
diff --git a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
index 87abf08f1de21253e3f317121bd2dcd64f3a7bed..a548539d44a7f3cf595f94bd7c218b27851b2cc3 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexMessagingIntegrationTest.java
@@ -4,6 +4,7 @@ import com.google.inject.Guice;
 import com.google.inject.Injector;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.ImmediateExecutor;
 import org.briarproject.TestDatabaseModule;
 import org.briarproject.TestSystemModule;
 import org.briarproject.TestUtils;
@@ -43,7 +44,6 @@ import org.briarproject.event.EventModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.lifecycle.LifecycleModule;
 import org.briarproject.messaging.MessagingModule;
-import org.briarproject.plugins.ImmediateExecutor;
 import org.briarproject.transport.TransportModule;
 import org.junit.After;
 import org.junit.Before;
diff --git a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
index 635b2b0685934562a494303500850b0900ecd98a..a62b7add1271d904c284c2af23d5313448a9a824 100644
--- a/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
+++ b/briar-tests/src/org/briarproject/sync/SimplexOutgoingSessionTest.java
@@ -1,6 +1,7 @@
 package org.briarproject.sync;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.ImmediateExecutor;
 import org.briarproject.TestUtils;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
@@ -10,7 +11,6 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.sync.Ack;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.PacketWriter;
-import org.briarproject.plugins.ImmediateExecutor;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
diff --git a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
index 4863c85c9abe5ab41d2ec08226c2a417e1e88b9c..b55bacd90694245b784f41b51f083a31c4186516 100644
--- a/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/sync/ValidationManagerImplTest.java
@@ -1,14 +1,285 @@
 package org.briarproject.sync;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.ImmediateExecutor;
+import org.briarproject.TestUtils;
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.db.NoSuchMessageException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.event.MessageAddedEvent;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.sync.MessageValidator;
+import org.briarproject.api.sync.ValidationManager.ValidationHook;
+import org.briarproject.util.ByteUtils;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
 import org.junit.Test;
 
-import static org.junit.Assert.fail;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
 
 public class ValidationManagerImplTest extends BriarTestCase {
 
+	private final ClientId clientId = new ClientId(TestUtils.getRandomId());
+	private final MessageId messageId = new MessageId(TestUtils.getRandomId());
+	private final MessageId messageId1 = new MessageId(TestUtils.getRandomId());
+	private final GroupId groupId = new GroupId(TestUtils.getRandomId());
+	private final byte[] descriptor = new byte[32];
+	private final Group group = new Group(groupId, clientId, descriptor);
+	private final long timestamp = System.currentTimeMillis();
+	private final byte[] raw = new byte[123];
+	private final Message message = new Message(messageId, groupId, timestamp,
+			raw);
+	private final Message message1 = new Message(messageId1, groupId, timestamp,
+			raw);
+	private final Metadata metadata = new Metadata();
+	private final ContactId contactId = new ContactId(234);
+
+	public ValidationManagerImplTest() {
+		// Encode the messages
+		System.arraycopy(groupId.getBytes(), 0, raw, 0, UniqueId.LENGTH);
+		ByteUtils.writeUint64(timestamp, raw, UniqueId.LENGTH);
+	}
+
+	@Test
+	public void testMessagesAreValidatedAtStartup() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final ValidationHook hook = context.mock(ValidationHook.class);
+		final Transaction txn = new Transaction(null);
+		final Transaction txn1 = new Transaction(null);
+		final Transaction txn2 = new Transaction(null);
+		final Transaction txn3 = new Transaction(null);
+		final Transaction txn4 = new Transaction(null);
+		context.checking(new Expectations() {{
+			// Get messages to validate
+			oneOf(db).startTransaction();
+			will(returnValue(txn));
+			oneOf(db).getMessagesToValidate(txn, clientId);
+			will(returnValue(Arrays.asList(messageId, messageId1)));
+			oneOf(db).endTransaction(txn);
+			// Load the first raw message and group
+			oneOf(db).startTransaction();
+			will(returnValue(txn1));
+			oneOf(db).getRawMessage(txn1, messageId);
+			will(returnValue(raw));
+			oneOf(db).getGroup(txn1, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn1);
+			// Validate the first message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(metadata));
+			// Store the validation result for the first message
+			oneOf(db).startTransaction();
+			will(returnValue(txn2));
+			oneOf(db).mergeMessageMetadata(txn2, messageId, metadata);
+			oneOf(db).setMessageValid(txn2, message, clientId, true);
+			oneOf(db).setMessageShared(txn2, message, true);
+			// Call the hook for the first message
+			oneOf(hook).validatingMessage(txn2, message, clientId, metadata);
+			oneOf(db).endTransaction(txn2);
+			// Load the second raw message and group
+			oneOf(db).startTransaction();
+			will(returnValue(txn3));
+			oneOf(db).getRawMessage(txn3, messageId1);
+			will(returnValue(raw));
+			oneOf(db).getGroup(txn3, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn3);
+			// Validate the second message: invalid
+			oneOf(validator).validateMessage(message1, group);
+			will(returnValue(null));
+			// Store the validation result for the second message
+			oneOf(db).startTransaction();
+			will(returnValue(txn4));
+			oneOf(db).setMessageValid(txn4, message1, clientId, false);
+			oneOf(db).endTransaction(txn4);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerValidationHook(hook);
+		vm.start();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testValidationContinuesAfterNoSuchMessageException()
+			throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final ValidationHook hook = context.mock(ValidationHook.class);
+		final Transaction txn = new Transaction(null);
+		final Transaction txn1 = new Transaction(null);
+		final Transaction txn2 = new Transaction(null);
+		final Transaction txn3 = new Transaction(null);
+		context.checking(new Expectations() {{
+			// Get messages to validate
+			oneOf(db).startTransaction();
+			will(returnValue(txn));
+			oneOf(db).getMessagesToValidate(txn, clientId);
+			will(returnValue(Arrays.asList(messageId, messageId1)));
+			oneOf(db).endTransaction(txn);
+			// Load the first raw message - *gasp* it's gone!
+			oneOf(db).startTransaction();
+			will(returnValue(txn1));
+			oneOf(db).getRawMessage(txn1, messageId);
+			will(throwException(new NoSuchMessageException()));
+			oneOf(db).endTransaction(txn1);
+			// Load the second raw message and group
+			oneOf(db).startTransaction();
+			will(returnValue(txn2));
+			oneOf(db).getRawMessage(txn2, messageId1);
+			will(returnValue(raw));
+			oneOf(db).getGroup(txn2, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn2);
+			// Validate the second message: invalid
+			oneOf(validator).validateMessage(message1, group);
+			will(returnValue(null));
+			// Store the validation result for the second message
+			oneOf(db).startTransaction();
+			will(returnValue(txn3));
+			oneOf(db).setMessageValid(txn3, message1, clientId, false);
+			oneOf(db).endTransaction(txn3);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerValidationHook(hook);
+		vm.start();
+
+		context.assertIsSatisfied();
+	}
+
 	@Test
-	public void testUnitTestsExist() {
-		fail(); // FIXME: Write tests
+	public void testValidationContinuesAfterNoSuchGroupException()
+			throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final ValidationHook hook = context.mock(ValidationHook.class);
+		final Transaction txn = new Transaction(null);
+		final Transaction txn1 = new Transaction(null);
+		final Transaction txn2 = new Transaction(null);
+		final Transaction txn3 = new Transaction(null);
+		context.checking(new Expectations() {{
+			// Get messages to validate
+			oneOf(db).startTransaction();
+			will(returnValue(txn));
+			oneOf(db).getMessagesToValidate(txn, clientId);
+			will(returnValue(Arrays.asList(messageId, messageId1)));
+			oneOf(db).endTransaction(txn);
+			// Load the first raw message
+			oneOf(db).startTransaction();
+			will(returnValue(txn1));
+			oneOf(db).getRawMessage(txn1, messageId);
+			will(returnValue(raw));
+			// Load the group - *gasp* it's gone!
+			oneOf(db).getGroup(txn1, groupId);
+			will(throwException(new NoSuchGroupException()));
+			oneOf(db).endTransaction(txn1);
+			// Load the second raw message and group
+			oneOf(db).startTransaction();
+			will(returnValue(txn2));
+			oneOf(db).getRawMessage(txn2, messageId1);
+			will(returnValue(raw));
+			oneOf(db).getGroup(txn2, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn2);
+			// Validate the second message: invalid
+			oneOf(validator).validateMessage(message1, group);
+			will(returnValue(null));
+			// Store the validation result for the second message
+			oneOf(db).startTransaction();
+			will(returnValue(txn3));
+			oneOf(db).setMessageValid(txn3, message1, clientId, false);
+			oneOf(db).endTransaction(txn3);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerValidationHook(hook);
+		vm.start();
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testNonLocalMessagesAreValidatedWhenAdded() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final ValidationHook hook = context.mock(ValidationHook.class);
+		final Transaction txn = new Transaction(null);
+		final Transaction txn1 = new Transaction(null);
+		context.checking(new Expectations() {{
+			// Load the group
+			oneOf(db).startTransaction();
+			will(returnValue(txn));
+			oneOf(db).getGroup(txn, groupId);
+			will(returnValue(group));
+			oneOf(db).endTransaction(txn);
+			// Validate the message: valid
+			oneOf(validator).validateMessage(message, group);
+			will(returnValue(metadata));
+			// Store the validation result
+			oneOf(db).startTransaction();
+			will(returnValue(txn1));
+			oneOf(db).mergeMessageMetadata(txn1, messageId, metadata);
+			oneOf(db).setMessageValid(txn1, message, clientId, true);
+			oneOf(db).setMessageShared(txn1, message, true);
+			// Call the hook
+			oneOf(hook).validatingMessage(txn1, message, clientId, metadata);
+			oneOf(db).endTransaction(txn1);
+		}});
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerValidationHook(hook);
+		vm.eventOccurred(new MessageAddedEvent(message, contactId));
+
+		context.assertIsSatisfied();
+	}
+
+	@Test
+	public void testLocalMessagesAreNotValidatedWhenAdded() throws Exception {
+		Mockery context = new Mockery();
+		final DatabaseComponent db = context.mock(DatabaseComponent.class);
+		final Executor dbExecutor = new ImmediateExecutor();
+		final Executor cryptoExecutor = new ImmediateExecutor();
+		final MessageValidator validator = context.mock(MessageValidator.class);
+		final ValidationHook hook = context.mock(ValidationHook.class);
+
+		ValidationManagerImpl vm = new ValidationManagerImpl(db, dbExecutor,
+				cryptoExecutor);
+		vm.registerMessageValidator(clientId, validator);
+		vm.registerValidationHook(hook);
+		vm.eventOccurred(new MessageAddedEvent(message, null));
+
+		context.assertIsSatisfied();
 	}
 }