diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
index 7e27d74685bdf55eb9fb23d27981b029d97796ad..76f5b6b3747b6aebb961d1907e2561166d2835ce 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
@@ -9,7 +9,6 @@ import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.nullsafety.NotNullByDefault;
-import org.briarproject.api.privategroup.MessageType;
 import org.briarproject.api.privategroup.PrivateGroup;
 import org.briarproject.api.privategroup.PrivateGroupFactory;
 import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
@@ -79,17 +78,14 @@ class GroupMessageValidator extends BdfMessageValidator {
 
 		Author member = authorFactory.createAuthor(memberName, memberPublicKey);
 		BdfMessageContext c;
-		switch (MessageType.valueOf(type)) {
-			case JOIN:
-				c = validateJoin(m, g, body, member);
-				addMessageMetadata(c, member, m.getTimestamp());
-				break;
-			case POST:
-				c = validatePost(m, g, body, member);
-				addMessageMetadata(c, member, m.getTimestamp());
-				break;
-			default:
-				throw new InvalidMessageException("Unknown Message Type");
+		if (type == JOIN.getInt()) {
+			c = validateJoin(m, g, body, member);
+			addMessageMetadata(c, member, m.getTimestamp());
+		} else if (type == POST.getInt()) {
+			c = validatePost(m, g, body, member);
+			addMessageMetadata(c, member, m.getTimestamp());
+		} else {
+			throw new InvalidMessageException("Unknown Message Type");
 		}
 		c.getDictionary().put(KEY_TYPE, type);
 		return c;
@@ -133,8 +129,9 @@ class GroupMessageValidator extends BdfMessageValidator {
 					.createInviteToken(creator.getId(), member.getId(),
 							pg.getId(), inviteTimestamp);
 			try {
-				clientHelper.verifySignature(SIGNING_LABEL_INVITE, creatorSignature,
-						creator.getPublicKey(), token);
+				clientHelper
+						.verifySignature(SIGNING_LABEL_INVITE, creatorSignature,
+								creator.getPublicKey(), token);
 			} catch (GeneralSecurityException e) {
 				throw new InvalidMessageException(e);
 			}
@@ -180,7 +177,7 @@ class GroupMessageValidator extends BdfMessageValidator {
 
 		// content (string)
 		String content = body.getString(5);
-		checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH);
+		checkLength(content, 1, MAX_GROUP_POST_BODY_LENGTH);
 
 		// signature (raw)
 		// a signature with the member's private key over a list with 7 elements
diff --git a/briar-tests/src/org/briarproject/ValidatorTestCase.java b/briar-tests/src/org/briarproject/ValidatorTestCase.java
index 4e5a3b5e62392abe2cd2333bf73e164c5eb24e57..f7a7693fdae94e2d1e3adb038f4b6517de899f05 100644
--- a/briar-tests/src/org/briarproject/ValidatorTestCase.java
+++ b/briar-tests/src/org/briarproject/ValidatorTestCase.java
@@ -2,6 +2,7 @@ package org.briarproject;
 
 import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
@@ -16,6 +17,8 @@ public abstract class ValidatorTestCase extends BriarMockTestCase {
 	protected final MetadataEncoder metadataEncoder =
 			context.mock(MetadataEncoder.class);
 	protected final Clock clock = context.mock(Clock.class);
+	protected final AuthorFactory authorFactory =
+			context.mock(AuthorFactory.class);
 
 	protected final MessageId messageId =
 			new MessageId(TestUtils.getRandomId());
diff --git a/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java b/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..cff74d31d34d043860c821705e297114a06cb253
--- /dev/null
+++ b/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java
@@ -0,0 +1,648 @@
+package org.briarproject.privategroup;
+
+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.identity.Author;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.privategroup.MessageType;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
+import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
+import org.briarproject.api.sync.InvalidMessageException;
+import org.briarproject.api.sync.MessageId;
+import org.jmock.Expectations;
+import org.junit.Test;
+
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.briarproject.TestUtils.getRandomBytes;
+import static org.briarproject.TestUtils.getRandomId;
+import static org.briarproject.TestUtils.getRandomString;
+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.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_JOIN;
+import static org.briarproject.api.privategroup.GroupMessageFactory.SIGNING_LABEL_POST;
+import static org.briarproject.api.privategroup.MessageType.JOIN;
+import static org.briarproject.api.privategroup.MessageType.POST;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
+import static org.briarproject.api.privategroup.invitation.GroupInvitationFactory.SIGNING_LABEL_INVITE;
+import static org.briarproject.privategroup.GroupConstants.KEY_INITIAL_JOIN_MSG;
+import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_ID;
+import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_NAME;
+import static org.briarproject.privategroup.GroupConstants.KEY_MEMBER_PUBLIC_KEY;
+import static org.briarproject.privategroup.GroupConstants.KEY_PARENT_MSG_ID;
+import static org.briarproject.privategroup.GroupConstants.KEY_PREVIOUS_MSG_ID;
+import static org.briarproject.privategroup.GroupConstants.KEY_READ;
+import static org.briarproject.privategroup.GroupConstants.KEY_TIMESTAMP;
+import static org.briarproject.privategroup.GroupConstants.KEY_TYPE;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class GroupMessageValidatorTest extends ValidatorTestCase {
+
+	private final PrivateGroupFactory privateGroupFactory =
+			context.mock(PrivateGroupFactory.class);
+	private final GroupInvitationFactory groupInvitationFactory =
+			context.mock(GroupInvitationFactory.class);
+
+	private final String creatorName = getRandomString(MAX_AUTHOR_NAME_LENGTH);
+	private final String memberName = getRandomString(MAX_AUTHOR_NAME_LENGTH);
+	private final byte[] creatorKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final byte[] memberKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final byte[] creatorSignature =
+			getRandomBytes(MAX_SIGNATURE_LENGTH);
+	private final byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH);
+	private final Author member =
+			new Author(new AuthorId(getRandomId()), memberName, memberKey);
+	private final Author creator =
+			new Author(new AuthorId(getRandomId()), creatorName, creatorKey);
+	private final long inviteTimestamp = 42L;
+	private final PrivateGroup privateGroup = new PrivateGroup(group,
+			getRandomString(MAX_GROUP_NAME_LENGTH), creator,
+			getRandomBytes(GROUP_SALT_LENGTH));
+	private final BdfList token = BdfList.of("token");
+	private final MessageId parentId = new MessageId(getRandomId());
+	private final MessageId previousMsgId = new MessageId(getRandomId());
+	private final String postContent =
+			getRandomString(MAX_GROUP_POST_BODY_LENGTH);
+
+	private final GroupMessageValidator validator =
+			new GroupMessageValidator(privateGroupFactory, clientHelper,
+					metadataEncoder, clock, authorFactory,
+					groupInvitationFactory);
+
+	// JOIN message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortJoinMessage() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), creatorName, creatorKey, null);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongJoinMessage() throws Exception {
+		expectCreateAuthor(creator);
+		BdfList body = BdfList.of(JOIN.getInt(), creatorName, creatorKey, null,
+				signature, "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithTooShortMemberName() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), "", memberKey, null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongMemberName() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(),
+				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), memberKey, null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNullMemberName() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), null, memberKey, null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNonStringMemberName() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), getRandomBytes(5), memberKey,
+				null, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithTooShortMemberKey() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, new byte[0], null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithTooLongMemberKey() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), memberName,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), null, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithNoullMemberKey() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, null, null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithNonRawMemberKey() throws Exception {
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, "not raw", null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithNonListInvitation() throws Exception {
+		expectCreateAuthor(creator);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				"not a list", signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsCreatorJoin() throws Exception {
+		expectJoinMessage(creator, null, true, true);
+		BdfList body = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				null, signature);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertExpectedMessageContext(messageContext, JOIN, creator,
+				Collections.<MessageId>emptyList());
+		assertTrue(messageContext.getDictionary()
+				.getBoolean(KEY_INITIAL_JOIN_MSG));
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinWithNullInvitation() throws Exception {
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, null,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithTooShortInvitation() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithTooLongInvitation() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature, "");
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinWithEqualInvitationTime()
+			throws Exception {
+		BdfList invite = BdfList.of(message.getTimestamp(), creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinWithLaterInvitationTime()
+			throws Exception {
+		BdfList invite = BdfList.of(message.getTimestamp() + 1,
+				creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithNullInvitationTime()
+			throws Exception {
+		BdfList invite = BdfList.of(null, creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithNonLongInvitationTime()
+			throws Exception {
+		BdfList invite = BdfList.of("not long", creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithTooShortCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, new byte[0]);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinWithTooLongCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp,
+				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithNullCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, null);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMemberJoinWithNonRawCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, "not raw");
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinWithInvalidCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, false, true);
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinWithInvalidMemberSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, true, false);
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsMemberJoin() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, true, true);
+		BdfList body = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertExpectedMessageContext(messageContext, JOIN, member,
+				Collections.<MessageId>emptyList());
+		assertFalse(messageContext.getDictionary()
+				.getBoolean(KEY_INITIAL_JOIN_MSG));
+	}
+
+	private void expectCreateAuthor(final Author member) {
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(member.getName(),
+					member.getPublicKey());
+			will(returnValue(member));
+		}});
+	}
+
+	private void expectParsePrivateGroup() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(privateGroupFactory).parsePrivateGroup(group);
+			will(returnValue(privateGroup));
+		}});
+	}
+
+	private void expectJoinMessage(final Author member, final BdfList invite,
+			final boolean creatorSigValid, final boolean memberSigValid)
+			throws Exception {
+		final BdfList signed = BdfList.of(group.getId(), message.getTimestamp(),
+				JOIN.getInt(), member.getName(), member.getPublicKey(), invite);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		context.checking(new Expectations() {{
+			if (invite != null) {
+				oneOf(groupInvitationFactory).createInviteToken(creator.getId(),
+						member.getId(), privateGroup.getId(), inviteTimestamp);
+				will(returnValue(token));
+				oneOf(clientHelper).verifySignature(SIGNING_LABEL_INVITE,
+						creatorSignature, creatorKey, token);
+				if (!memberSigValid)
+					will(throwException(new GeneralSecurityException()));
+			}
+			if (memberSigValid) {
+				oneOf(clientHelper).verifySignature(SIGNING_LABEL_JOIN,
+						signature, member.getPublicKey(), signed);
+				if (!creatorSigValid)
+					will(throwException(new GeneralSecurityException()));
+			}
+		}});
+	}
+
+	// POST Message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortPost() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongPost() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent, signature, "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortMemberName() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), "", memberKey, parentId,
+				previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongMemberName() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(),
+				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), memberKey,
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNullMemberName() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), null, memberKey,
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonStringMemberName() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), getRandomBytes(5), memberKey,
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortMemberKey() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, new byte[0],
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongMemberKey() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), parentId,
+				previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNullMemberKey() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, null,
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawMemberKey() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, "not raw",
+				parentId, previousMsgId, postContent, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortParentId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				getRandomBytes(MessageId.LENGTH - 1), previousMsgId,
+				postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongParentId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				getRandomBytes(MessageId.LENGTH + 1), previousMsgId,
+				postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawParentId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				"not raw", previousMsgId, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortPreviousMsgId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, getRandomBytes(MessageId.LENGTH - 1), postContent,
+				signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongPreviousMsgId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, getRandomBytes(MessageId.LENGTH + 1), postContent,
+				signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNullPreviousMsgId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, null, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawPreviousMsgId() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, "not raw", postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortContent() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, "", signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongContent() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId,
+				getRandomString(MAX_GROUP_POST_BODY_LENGTH + 1), signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNullContent() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, null, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonStringContent() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, getRandomBytes(5), signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortSignature() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent, new byte[0]);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongSignature() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent,
+				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNullSignature() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent,null);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawSignature() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent, "not raw");
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsPostWithInvalidSignature() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent, signature);
+		expectPostMessage(member, parentId, false);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsPost() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey,
+				parentId, previousMsgId, postContent, signature);
+		expectPostMessage(member, parentId, true);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertExpectedMessageContext(messageContext, POST, member,
+				Arrays.asList(parentId, previousMsgId));
+		assertArrayEquals(previousMsgId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
+		assertArrayEquals(parentId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PARENT_MSG_ID));
+	}
+
+	@Test
+	public void testAcceptsTopLevelPost() throws Exception {
+		BdfList body = BdfList.of(POST.getInt(), memberName, memberKey, null,
+				previousMsgId, postContent, signature);
+		expectPostMessage(member, null, true);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertExpectedMessageContext(messageContext, POST, member,
+				Collections.singletonList(previousMsgId));
+		assertArrayEquals(previousMsgId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
+		assertFalse(
+				messageContext.getDictionary().containsKey(KEY_PARENT_MSG_ID));
+	}
+
+	private void expectPostMessage(final Author member,
+			final MessageId parentId, final boolean sigValid) throws Exception {
+		final BdfList signed = BdfList.of(group.getId(), message.getTimestamp(),
+				POST.getInt(), member.getName(), member.getPublicKey(),
+				parentId == null ? null : parentId.getBytes(),
+				previousMsgId.getBytes(), postContent);
+		expectCreateAuthor(member);
+		context.checking(new Expectations() {{
+			oneOf(clientHelper).verifySignature(SIGNING_LABEL_POST, signature,
+					member.getPublicKey(), signed);
+			if (!sigValid) will(throwException(new GeneralSecurityException()));
+		}});
+	}
+
+	private void assertExpectedMessageContext(BdfMessageContext c,
+			MessageType type, Author member,
+			Collection<MessageId> dependencies) throws FormatException {
+		BdfDictionary d = c.getDictionary();
+		assertEquals(type.getInt(), d.getLong(KEY_TYPE).intValue());
+		assertEquals(message.getTimestamp(),
+				d.getLong(KEY_TIMESTAMP).longValue());
+		assertFalse(d.getBoolean(KEY_READ));
+		assertEquals(member.getId().getBytes(), d.getRaw(KEY_MEMBER_ID));
+		assertEquals(member.getName(), d.getString(KEY_MEMBER_NAME));
+		assertEquals(member.getPublicKey(), d.getRaw(KEY_MEMBER_PUBLIC_KEY));
+		assertEquals(dependencies, c.getDependencies());
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMessageWithUnknownType() throws Exception {
+		BdfList body = BdfList.of(POST.getInt() + 1, memberName, memberKey,
+				parentId, previousMsgId, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, body);
+	}
+}
diff --git a/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java b/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..76ed641b4219ef855da1143afc41eb669f9af59e
--- /dev/null
+++ b/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java
@@ -0,0 +1,582 @@
+package org.briarproject.privategroup.invitation;
+
+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.BdfEntry;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.privategroup.PrivateGroup;
+import org.briarproject.api.privategroup.PrivateGroupFactory;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.MessageId;
+import org.jmock.Expectations;
+import org.junit.Test;
+
+import java.security.GeneralSecurityException;
+
+import static org.briarproject.TestUtils.getRandomBytes;
+import static org.briarproject.TestUtils.getRandomId;
+import static org.briarproject.TestUtils.getRandomString;
+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.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
+import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
+import static org.briarproject.api.privategroup.invitation.GroupInvitationFactory.SIGNING_LABEL_INVITE;
+import static org.briarproject.privategroup.invitation.MessageType.ABORT;
+import static org.briarproject.privategroup.invitation.MessageType.INVITE;
+import static org.briarproject.privategroup.invitation.MessageType.JOIN;
+import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class GroupInvitationValidatorTest extends ValidatorTestCase {
+
+	private final PrivateGroupFactory privateGroupFactory =
+			context.mock(PrivateGroupFactory.class);
+	private final MessageEncoder messageEncoder =
+			context.mock(MessageEncoder.class);
+
+	private final String groupName = getRandomString(MAX_GROUP_NAME_LENGTH);
+	private final String creatorName = getRandomString(MAX_AUTHOR_NAME_LENGTH);
+	private final byte[] creatorKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final Author creator =
+			new Author(new AuthorId(getRandomId()), creatorName, creatorKey);
+	private final byte[] salt = getRandomBytes(GROUP_SALT_LENGTH);
+	private final PrivateGroup privateGroup =
+			new PrivateGroup(group, groupName, creator, salt);
+	private final String inviteText =
+			getRandomString(MAX_GROUP_INVITATION_MSG_LENGTH);
+	private final byte[] signature = getRandomBytes(MAX_SIGNATURE_LENGTH);
+	private final BdfDictionary meta =
+			BdfDictionary.of(new BdfEntry("meta", "data"));
+	private final MessageId previousMessageId = new MessageId(getRandomId());
+
+	private final GroupInvitationValidator validator =
+			new GroupInvitationValidator(clientHelper, metadataEncoder,
+					clock, authorFactory, privateGroupFactory, messageEncoder);
+
+	// INVITE Message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortInviteMessage() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongInviteMessage() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, signature, "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortGroupName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), "", creatorName,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongGroupName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(),
+				getRandomString(MAX_GROUP_NAME_LENGTH + 1), creatorName,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullGroupName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), null, creatorName,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonStringGroupName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), getRandomBytes(5),
+				creatorName, creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortCreatorName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, "", creatorKey,
+				salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongCreatorName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName,
+				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), creatorKey, salt,
+				inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullCreatorName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, null,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonStringCreatorName()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName,
+				getRandomBytes(5), creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortCreatorKey()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				new byte[0], salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongCreatorKey()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), salt, inviteText,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullCreatorKey()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				null, salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonRawCreatorKey()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				"not raw", salt, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortGroupSalt()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, getRandomBytes(GROUP_SALT_LENGTH - 1), inviteText,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongGroupSalt()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, getRandomBytes(GROUP_SALT_LENGTH + 1), inviteText,
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullGroupSalt()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, null, inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonRawGroupSalt()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, "not raw", inviteText, signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortContent() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, "", signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongContent() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt,
+				getRandomString(MAX_GROUP_INVITATION_MSG_LENGTH + 1),
+				signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsInviteMessageWithNullContent() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, null, signature);
+		expectInviteMessage(false);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonStringContent()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, getRandomBytes(5), signature);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortSignature()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, new byte[0]);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongSignature()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText,
+				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullSignature()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, null);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonRawSignature()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, "not raw");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithInvalidSignature()
+			throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, null, signature);
+		expectInviteMessage(true);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsValidInviteMessage() throws Exception {
+		BdfList body = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, signature);
+		expectInviteMessage(false);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertTrue(messageContext.getDependencies().isEmpty());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	private void expectInviteMessage(final boolean exception) throws Exception {
+		final BdfList signed = BdfList.of(message.getTimestamp(),
+				message.getGroupId(), privateGroup.getId());
+		context.checking(new Expectations() {{
+			oneOf(authorFactory).createAuthor(creatorName, creatorKey);
+			will(returnValue(creator));
+			oneOf(privateGroupFactory).createPrivateGroup(groupName, creator,
+					salt);
+			will(returnValue(privateGroup));
+			oneOf(clientHelper).verifySignature(SIGNING_LABEL_INVITE, signature,
+					creatorKey, signed);
+			if (exception) {
+				will(throwException(new GeneralSecurityException()));
+			} else {
+				oneOf(messageEncoder).encodeMetadata(INVITE,
+						message.getGroupId(), message.getTimestamp(), false,
+						false, false, false);
+				will(returnValue(meta));
+			}
+		}});
+	}
+
+	// JOIN Message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortJoinMessage() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId());
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongJoinMessage() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				previousMessageId, "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortGroupId() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(),
+				getRandomBytes(GroupId.LENGTH - 1), previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongGroupId() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(),
+				getRandomBytes(GroupId.LENGTH + 1), previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNullGroupId() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), null, previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNonRawGroupId() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), "not raw",
+				previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH - 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH + 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNonRawPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				"not raw");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsJoinMessageWithNullPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(), null);
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(JOIN, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	@Test
+	public void testAcceptsValidJoinMessage() throws Exception {
+		BdfList body = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				previousMessageId);
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(JOIN, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertEquals(1, messageContext.getDependencies().size());
+		assertEquals(previousMessageId,
+				messageContext.getDependencies().iterator().next());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	// LEAVE message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortLeaveMessage() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId());
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongLeaveMessage() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				previousMessageId, "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooShortGroupId() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(),
+				getRandomBytes(GroupId.LENGTH - 1), previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooLongGroupId() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(),
+				getRandomBytes(GroupId.LENGTH + 1), previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithNullGroupId() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), null, previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithNonRawGroupId() throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), "not raw",
+				previousMessageId);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooShortPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH - 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooLongPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH + 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithNonRawPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				"not raw");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsLeaveMessageWithNullPreviousMessageId()
+			throws Exception {
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(), null);
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(LEAVE, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	@Test
+	public void testAcceptsValidLeaveMessage() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(LEAVE, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfList body = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				previousMessageId);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertEquals(1, messageContext.getDependencies().size());
+		assertEquals(previousMessageId,
+				messageContext.getDependencies().iterator().next());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	// ABORT message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortAbortMessage() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue());
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongAbortMessage() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), privateGroup.getId(), "");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithTooShortGroupId() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(),
+				getRandomBytes(GroupId.LENGTH - 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithTooLongGroupId() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(),
+				getRandomBytes(GroupId.LENGTH + 1));
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithNullGroupId() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), null);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithNonRawGroupId() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue(), "not raw");
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test
+	public void testAcceptsValidAbortMessage() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(ABORT, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfList body = BdfList.of(ABORT.getValue(), privateGroup.getId());
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, body);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertEquals(meta, messageContext.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMessageWithUnknownType() throws Exception {
+		BdfList body = BdfList.of(ABORT.getValue() + 1);
+		validator.validateMessage(message, group, body);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsEmptyMessage() throws Exception {
+		BdfList body = new BdfList();
+		validator.validateMessage(message, group, body);
+	}
+
+}