diff --git a/briar-tests/src/org/briarproject/BriarMockTestCase.java b/briar-tests/src/org/briarproject/BriarMockTestCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..45b662013b6050e1c12b554f1437c674aa13a4e5
--- /dev/null
+++ b/briar-tests/src/org/briarproject/BriarMockTestCase.java
@@ -0,0 +1,14 @@
+package org.briarproject;
+
+import org.jmock.Mockery;
+import org.junit.After;
+
+public abstract class BriarMockTestCase extends BriarTestCase {
+
+	protected final Mockery context = new Mockery();
+
+	@After
+	public void checkExpectations() {
+		context.assertIsSatisfied();
+	}
+}
diff --git a/briar-tests/src/org/briarproject/ValidatorTestCase.java b/briar-tests/src/org/briarproject/ValidatorTestCase.java
new file mode 100644
index 0000000000000000000000000000000000000000..4e5a3b5e62392abe2cd2333bf73e164c5eb24e57
--- /dev/null
+++ b/briar-tests/src/org/briarproject/ValidatorTestCase.java
@@ -0,0 +1,32 @@
+package org.briarproject;
+
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.data.MetadataEncoder;
+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.system.Clock;
+
+public abstract class ValidatorTestCase extends BriarMockTestCase {
+
+	protected final ClientHelper clientHelper =
+			context.mock(ClientHelper.class);
+	protected final MetadataEncoder metadataEncoder =
+			context.mock(MetadataEncoder.class);
+	protected final Clock clock = context.mock(Clock.class);
+
+	protected final MessageId messageId =
+			new MessageId(TestUtils.getRandomId());
+	protected final GroupId groupId = new GroupId(TestUtils.getRandomId());
+	protected final long timestamp = 1234567890 * 1000L;
+	protected final byte[] raw = TestUtils.getRandomBytes(123);
+	protected final Message message =
+			new Message(messageId, groupId, timestamp, raw);
+	protected final ClientId clientId =
+			new ClientId(TestUtils.getRandomString(123));
+	protected final byte[] descriptor = TestUtils.getRandomBytes(123);
+	protected final Group group = new Group(groupId, clientId, descriptor);
+
+}
diff --git a/briar-tests/src/org/briarproject/clients/BdfMessageValidatorTest.java b/briar-tests/src/org/briarproject/clients/BdfMessageValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..1714b261b3ac6713776d851cd1974fe50d9891b2
--- /dev/null
+++ b/briar-tests/src/org/briarproject/clients/BdfMessageValidatorTest.java
@@ -0,0 +1,165 @@
+package org.briarproject.clients;
+
+import org.briarproject.ValidatorTestCase;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.InvalidMessageException;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageContext;
+import org.jmock.Expectations;
+import org.jmock.lib.legacy.ClassImposteriser;
+import org.junit.Test;
+
+import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+public class BdfMessageValidatorTest extends ValidatorTestCase {
+
+	private final BdfMessageValidator subclassNotCalled =
+			new BdfMessageValidator(clientHelper, metadataEncoder, clock) {
+				@Override
+				protected BdfMessageContext validateMessage(Message m, Group g,
+						BdfList body)
+						throws InvalidMessageException, FormatException {
+					fail();
+					return null;
+				}
+			};
+
+	private final BdfList body = BdfList.of(123, 456);
+	private final BdfDictionary dictionary = new BdfDictionary();
+	private final Metadata meta = new Metadata();
+
+	public BdfMessageValidatorTest() {
+		context.setImposteriser(ClassImposteriser.INSTANCE);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsFarFutureTimestamp() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp - MAX_CLOCK_DIFFERENCE - 1));
+		}});
+
+		subclassNotCalled.validateMessage(message, group);
+	}
+
+	@Test
+	public void testAcceptsMaxTimestamp() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp - MAX_CLOCK_DIFFERENCE));
+			oneOf(clientHelper).toList(raw, MESSAGE_HEADER_LENGTH,
+					raw.length - MESSAGE_HEADER_LENGTH);
+			will(returnValue(body));
+			oneOf(metadataEncoder).encode(dictionary);
+			will(returnValue(meta));
+		}});
+
+		BdfMessageValidator v = new BdfMessageValidator(clientHelper,
+				metadataEncoder, clock) {
+			@Override
+			protected BdfMessageContext validateMessage(Message m, Group g,
+					BdfList b) throws InvalidMessageException, FormatException {
+				assertSame(message, m);
+				assertSame(group, g);
+				assertSame(body, b);
+				return new BdfMessageContext(dictionary);
+			}
+		};
+		MessageContext messageContext = v.validateMessage(message, group);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertSame(meta, messageContext.getMetadata());
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsTooShortMessage() throws Exception {
+		final byte[] invalidRaw = new byte[MESSAGE_HEADER_LENGTH];
+		// Use a mock message so the length of the raw message can be invalid
+		final Message invalidMessage = context.mock(Message.class);
+
+		context.checking(new Expectations() {{
+			oneOf(invalidMessage).getTimestamp();
+			will(returnValue(timestamp));
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp));
+			oneOf(invalidMessage).getRaw();
+			will(returnValue(invalidRaw));
+		}});
+
+		subclassNotCalled.validateMessage(invalidMessage, group);
+	}
+
+	@Test
+	public void testAcceptsMinLengthMessage() throws Exception {
+		final byte[] shortRaw = new byte[MESSAGE_HEADER_LENGTH + 1];
+		final Message shortMessage =
+				new Message(messageId, groupId, timestamp, shortRaw);
+
+		context.checking(new Expectations() {{
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp));
+			oneOf(clientHelper).toList(shortRaw, MESSAGE_HEADER_LENGTH,
+					shortRaw.length - MESSAGE_HEADER_LENGTH);
+			will(returnValue(body));
+			oneOf(metadataEncoder).encode(dictionary);
+			will(returnValue(meta));
+		}});
+
+		BdfMessageValidator v = new BdfMessageValidator(clientHelper,
+				metadataEncoder, clock) {
+			@Override
+			protected BdfMessageContext validateMessage(Message m, Group g,
+					BdfList b) throws InvalidMessageException, FormatException {
+				assertSame(shortMessage, m);
+				assertSame(group, g);
+				assertSame(body, b);
+				return new BdfMessageContext(dictionary);
+			}
+		};
+		MessageContext messageContext = v.validateMessage(shortMessage, group);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertSame(meta, messageContext.getMetadata());
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsInvalidBdfList() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp));
+			oneOf(clientHelper).toList(raw, MESSAGE_HEADER_LENGTH,
+					raw.length - MESSAGE_HEADER_LENGTH);
+			will(throwException(new FormatException()));
+		}});
+
+		subclassNotCalled.validateMessage(message, group);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRethrowsFormatExceptionFromSubclass() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(clock).currentTimeMillis();
+			will(returnValue(timestamp));
+			oneOf(clientHelper).toList(raw, MESSAGE_HEADER_LENGTH,
+					raw.length - MESSAGE_HEADER_LENGTH);
+			will(returnValue(body));
+		}});
+
+		BdfMessageValidator v = new BdfMessageValidator(clientHelper,
+				metadataEncoder, clock) {
+			@Override
+			protected BdfMessageContext validateMessage(Message m, Group g,
+					BdfList b) throws InvalidMessageException, FormatException {
+				throw new FormatException();
+			}
+		};
+		v.validateMessage(message, group);
+	}
+}
diff --git a/briar-tests/src/org/briarproject/forum/ForumPostValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumPostValidatorTest.java
index 089cce54d979cdc067eba10510e9dcdcd4494bbd..17e5c1266dca74739f381392f5deb9815fa34dfb 100644
--- a/briar-tests/src/org/briarproject/forum/ForumPostValidatorTest.java
+++ b/briar-tests/src/org/briarproject/forum/ForumPostValidatorTest.java
@@ -1,14 +1,394 @@
 package org.briarproject.forum;
 
-import org.briarproject.BriarTestCase;
+import org.briarproject.TestUtils;
+import org.briarproject.ValidatorTestCase;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.InvalidMessageException;
+import org.briarproject.api.sync.MessageId;
+import org.jmock.Expectations;
 import org.junit.Test;
 
-import static org.junit.Assert.fail;
+import java.security.GeneralSecurityException;
+import java.util.Collection;
 
-public class ForumPostValidatorTest extends BriarTestCase {
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class ForumPostValidatorTest extends ValidatorTestCase {
+
+	private final AuthorFactory authorFactory =
+			context.mock(AuthorFactory.class);
+
+	private final MessageId parentId = new MessageId(TestUtils.getRandomId());
+	private final String authorName =
+			TestUtils.getRandomString(MAX_AUTHOR_NAME_LENGTH);
+	private final byte[] authorPublicKey =
+			TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final BdfList authorList = BdfList.of(authorName, authorPublicKey);
+	private final String content =
+			TestUtils.getRandomString(MAX_FORUM_POST_BODY_LENGTH);
+	private final byte[] signature =
+			TestUtils.getRandomBytes(MAX_SIGNATURE_LENGTH);
+	private final AuthorId authorId = new AuthorId(TestUtils.getRandomId());
+	private final Author author =
+			new Author(authorId, authorName, authorPublicKey);
+	private final BdfList signedWithParent = BdfList.of(groupId, timestamp,
+			parentId.getBytes(), authorList, content);
+	private final BdfList signedWithoutParent = BdfList.of(groupId, timestamp,
+			null, authorList, content);
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBody() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBody() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, signature, 123));
+	}
+
+	@Test
+	public void testAcceptsNullParentId() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+			oneOf(clientHelper).verifySignature(signature, authorPublicKey,
+					signedWithoutParent);
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(null, authorList, content, signature));
+		assertExpectedContext(messageContext, false, authorName);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonRawParentId() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(123, authorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortParentId() throws Exception {
+		byte[] invalidParentId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(invalidParentId, authorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongParentId() throws Exception {
+		byte[] invalidParentId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(invalidParentId, authorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullAuthorList() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, null, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonListAuthorList() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, 123, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortAuthorList() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, new BdfList(), content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongAuthorList() throws Exception {
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, BdfList.of(1, 2, 3), content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullAuthorName() throws Exception {
+		BdfList invalidAuthorList = BdfList.of(null, authorPublicKey);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringAuthorName() throws Exception {
+		BdfList invalidAuthorList = BdfList.of(123, authorPublicKey);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortAuthorName() throws Exception {
+		BdfList invalidAuthorList = BdfList.of("", authorPublicKey);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test
+	public void testAcceptsMinLengthAuthorName() throws Exception {
+		final String shortAuthorName = TestUtils.getRandomString(1);
+		BdfList shortNameAuthorList =
+				BdfList.of(shortAuthorName, authorPublicKey);
+		final Author shortNameAuthor =
+				new Author(authorId, shortAuthorName, authorPublicKey);
+		final BdfList signedWithShortNameAuthor = BdfList.of(groupId, timestamp,
+				parentId.getBytes(), shortNameAuthorList, content);
+
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(shortAuthorName, authorPublicKey);
+			will(returnValue(shortNameAuthor));
+			oneOf(clientHelper).verifySignature(signature, authorPublicKey,
+					signedWithShortNameAuthor);
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(parentId, shortNameAuthorList, content, signature));
+		assertExpectedContext(messageContext, true, shortAuthorName);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongAuthorName() throws Exception {
+		String invalidAuthorName =
+				TestUtils.getRandomString(MAX_AUTHOR_NAME_LENGTH + 1);
+		BdfList invalidAuthorList =
+				BdfList.of(invalidAuthorName, authorPublicKey);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullAuthorPublicKey() throws Exception {
+		BdfList invalidAuthorList = BdfList.of(authorName, null);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonRawAuthorPublicKey() throws Exception {
+		BdfList invalidAuthorList = BdfList.of(authorName, 123);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongAuthorPublicKey() throws Exception {
+		byte[] invalidAuthorPublicKey =
+				TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1);
+		BdfList invalidAuthorList =
+				BdfList.of(authorName, invalidAuthorPublicKey);
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, invalidAuthorList, content, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullContent() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, null, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringContent() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, 123, signature));
+	}
 
 	@Test
-	public void testUnitTestsExist() {
-		fail(); // FIXME: Write tests
+	public void testAcceptsMinLengthContent() throws Exception {
+		String shortContent = "";
+		final BdfList signedWithShortContent = BdfList.of(groupId, timestamp,
+				parentId.getBytes(), authorList, shortContent);
+
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+			oneOf(clientHelper).verifySignature(signature, authorPublicKey,
+					signedWithShortContent);
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, shortContent, signature));
+		assertExpectedContext(messageContext, true, authorName);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongContent() throws Exception {
+		String invalidContent =
+				TestUtils.getRandomString(MAX_FORUM_POST_BODY_LENGTH + 1);
+
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, invalidContent, signature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullSignature() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, null));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonRawSignature() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, 123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongSignature() throws Exception {
+		byte[] invalidSignature =
+				TestUtils.getRandomBytes(MAX_SIGNATURE_LENGTH + 1);
+
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, invalidSignature));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsIfVerifyingSignatureThrowsFormatException()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+			oneOf(clientHelper).verifySignature(signature, authorPublicKey,
+					signedWithParent);
+			will(throwException(new FormatException()));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, signature));
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsIfVerifyingSignatureThrowsGeneralSecurityException()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(authorName, authorPublicKey);
+			will(returnValue(author));
+			oneOf(clientHelper).verifySignature(signature, authorPublicKey,
+					signedWithParent);
+			will(throwException(new GeneralSecurityException()));
+		}});
+
+		ForumPostValidator v = new ForumPostValidator(authorFactory,
+				clientHelper, metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(parentId, authorList, content, signature));
+	}
+
+	private void assertExpectedContext(BdfMessageContext messageContext,
+			boolean hasParent, String authorName) throws FormatException {
+		BdfDictionary meta = messageContext.getDictionary();
+		Collection<MessageId> dependencies = messageContext.getDependencies();
+		if (hasParent) {
+			assertEquals(4, meta.size());
+			assertArrayEquals(parentId.getBytes(), meta.getRaw("parent"));
+			assertEquals(1, dependencies.size());
+			assertEquals(parentId, dependencies.iterator().next());
+		} else {
+			assertEquals(3, meta.size());
+			assertEquals(0, dependencies.size());
+		}
+		assertEquals(timestamp, meta.getLong("timestamp").longValue());
+		assertFalse(meta.getBoolean("read"));
+		BdfDictionary authorMeta = meta.getDictionary("author");
+		assertEquals(3, authorMeta.size());
+		assertArrayEquals(authorId.getBytes(), authorMeta.getRaw("id"));
+		assertEquals(authorName, authorMeta.getString("name"));
+		assertArrayEquals(authorPublicKey, authorMeta.getRaw("publicKey"));
 	}
 }
diff --git a/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java
deleted file mode 100644
index 0b87d04c19c36a768b5fcf4222b1445d5cffdaf1..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/forum/ForumSharingValidatorTest.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.briarproject.forum;
-
-import org.briarproject.BriarTestCase;
-import org.junit.Test;
-
-import static org.junit.Assert.fail;
-
-public class ForumSharingValidatorTest extends BriarTestCase {
-
-	@Test
-	public void testUnitTestsExist() {
-		fail(); // FIXME: Write tests
-	}
-}
diff --git a/briar-tests/src/org/briarproject/messaging/PrivateMessageValidatorTest.java b/briar-tests/src/org/briarproject/messaging/PrivateMessageValidatorTest.java
index 28668520ff6ba9a821226a93e24d50cc4131494c..3d5fad97a6c95ea9335f8b67ebcaf116fd0104d4 100644
--- a/briar-tests/src/org/briarproject/messaging/PrivateMessageValidatorTest.java
+++ b/briar-tests/src/org/briarproject/messaging/PrivateMessageValidatorTest.java
@@ -1,14 +1,84 @@
 package org.briarproject.messaging;
 
-import org.briarproject.BriarTestCase;
+import org.briarproject.TestUtils;
+import org.briarproject.ValidatorTestCase;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
 import org.junit.Test;
 
-import static org.junit.Assert.fail;
+import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
+import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 
-public class PrivateMessageValidatorTest extends BriarTestCase {
+public class PrivateMessageValidatorTest extends ValidatorTestCase {
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBody() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, new BdfList());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBody() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of("", 123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullContent() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of((String) null));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringContent() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of(123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongContent() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		String invalidContent =
+				TestUtils.getRandomString(MAX_PRIVATE_MESSAGE_BODY_LENGTH + 1);
+		v.validateMessage(message, group, BdfList.of(invalidContent));
+	}
 
 	@Test
-	public void testUnitTestsExist() {
-		fail(); // FIXME: Write tests
+	public void testAcceptsMaxLengthContent() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		String content =
+				TestUtils.getRandomString(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+		BdfMessageContext messageContext =
+				v.validateMessage(message, group, BdfList.of(content));
+		assertExpectedContext(messageContext);
+	}
+
+	@Test
+	public void testAcceptsMinLengthContent() throws Exception {
+		PrivateMessageValidator v = new PrivateMessageValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext =
+				v.validateMessage(message, group, BdfList.of(""));
+		assertExpectedContext(messageContext);
+	}
+
+	private void assertExpectedContext(BdfMessageContext messageContext)
+			throws FormatException {
+		BdfDictionary meta = messageContext.getDictionary();
+		assertEquals(3, meta.size());
+		assertEquals(timestamp, meta.getLong("timestamp").longValue());
+		assertFalse(meta.getBoolean("local"));
+		assertFalse(meta.getBoolean(MSG_KEY_READ));
+		assertEquals(0, messageContext.getDependencies().size());
 	}
 }
diff --git a/briar-tests/src/org/briarproject/sharing/ForumSharingValidatorTest.java b/briar-tests/src/org/briarproject/sharing/ForumSharingValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d3c6e22b25012af428b554a5c7018419643e53c
--- /dev/null
+++ b/briar-tests/src/org/briarproject/sharing/ForumSharingValidatorTest.java
@@ -0,0 +1,343 @@
+package org.briarproject.sharing;
+
+import org.briarproject.TestUtils;
+import org.briarproject.ValidatorTestCase;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.clients.BdfMessageContext;
+import org.briarproject.api.clients.SessionId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.junit.Test;
+
+import javax.annotation.Nullable;
+
+import static org.briarproject.api.forum.ForumConstants.FORUM_NAME;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
+import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
+import static org.briarproject.api.sharing.SharingConstants.INVITATION_MSG;
+import static org.briarproject.api.sharing.SharingConstants.LOCAL;
+import static org.briarproject.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
+import static org.briarproject.api.sharing.SharingConstants.SESSION_ID;
+import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
+import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
+import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
+import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
+import static org.briarproject.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
+import static org.briarproject.api.sharing.SharingConstants.TIME;
+import static org.briarproject.api.sharing.SharingConstants.TYPE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class ForumSharingValidatorTest extends ValidatorTestCase {
+
+	private final SessionId sessionId = new SessionId(TestUtils.getRandomId());
+	private final String forumName =
+			TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH);
+	private final byte[] salt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH);
+	private final String content =
+			TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH);
+
+	@Test
+	public void testAcceptsInvitationWithContent() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, content));
+		assertExpectedContextForInvitation(messageContext, forumName, content);
+	}
+
+	@Test
+	public void testAcceptsInvitationWithoutContent() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt));
+		assertExpectedContextForInvitation(messageContext, forumName, null);
+	}
+
+	@Test
+	public void testAcceptsAccept() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ACCEPT, sessionId));
+		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ACCEPT);
+	}
+
+	@Test
+	public void testAcceptsDecline() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_DECLINE, sessionId));
+		assertExpectedContext(messageContext, SHARE_MSG_TYPE_DECLINE);
+	}
+
+	@Test
+	public void testAcceptsLeave() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_LEAVE, sessionId));
+		assertExpectedContext(messageContext, SHARE_MSG_TYPE_LEAVE);
+	}
+
+	@Test
+	public void testAcceptsAbort() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId));
+		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ABORT);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullMessageType() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of(null, sessionId));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonLongMessageType() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of("", sessionId));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInvalidMessageType() throws Exception {
+		int invalidMessageType = SHARE_MSG_TYPE_ABORT + 1;
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(invalidMessageType, sessionId));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullSessionId() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, null));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonRawSessionId() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, 123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortSessionId() throws Exception {
+		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongSessionId() throws Exception {
+		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForAbort() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group, BdfList.of(SHARE_MSG_TYPE_ABORT));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBodyForAbort() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId, 123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortBodyForInvitation() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongBodyForInvitation() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, content, 123));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullForumName() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, null,
+						salt, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringForumName() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, 123,
+						salt, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortForumName() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, "",
+						salt, content));
+	}
+
+	@Test
+	public void testAcceptsMinLengthForumName() throws Exception {
+		String shortForumName = TestUtils.getRandomString(1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, shortForumName,
+						salt, content));
+		assertExpectedContextForInvitation(messageContext, shortForumName,
+				content);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongForumName() throws Exception {
+		String invalidForumName =
+				TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH + 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId,
+						invalidForumName, salt, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullSalt() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						null, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonRawSalt() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						123, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortSalt() throws Exception {
+		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH - 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						invalidSalt, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongSalt() throws Exception {
+		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH + 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						invalidSalt, content));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNullContent() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, null));
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsNonStringContent() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, 123));
+	}
+
+	@Test
+	public void testAcceptsMinLengthContent() throws Exception {
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, ""));
+		assertExpectedContextForInvitation(messageContext, forumName, "");
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongContent() throws Exception {
+		String invalidContent =
+				TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH + 1);
+		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
+				metadataEncoder, clock);
+		v.validateMessage(message, group,
+				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
+						salt, invalidContent));
+	}
+
+	private void assertExpectedContextForInvitation(
+			BdfMessageContext messageContext, String forumName,
+			@Nullable String content) throws FormatException {
+		BdfDictionary meta = messageContext.getDictionary();
+		if (content == null) {
+			assertEquals(6, meta.size());
+		} else {
+			assertEquals(7, meta.size());
+			assertEquals(content, meta.getString(INVITATION_MSG));
+		}
+		assertEquals(forumName, meta.getString(FORUM_NAME));
+		assertEquals(salt, meta.getRaw(FORUM_SALT));
+		assertEquals(SHARE_MSG_TYPE_INVITATION, meta.getLong(TYPE).intValue());
+		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
+		assertFalse(meta.getBoolean(LOCAL));
+		assertEquals(timestamp, meta.getLong(TIME).longValue());
+		assertEquals(0, messageContext.getDependencies().size());
+	}
+
+	private void assertExpectedContext(BdfMessageContext messageContext,
+			int type) throws FormatException {
+		BdfDictionary meta = messageContext.getDictionary();
+		assertEquals(4, meta.size());
+		assertEquals(type, meta.getLong(TYPE).intValue());
+		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
+		assertFalse(meta.getBoolean(LOCAL));
+		assertEquals(timestamp, meta.getLong(TIME).longValue());
+		assertEquals(0, messageContext.getDependencies().size());
+	}
+}