diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
index 7e27d74685bdf55eb9fb23d27981b029d97796ad..39b77cd2743f4b49d4f944365cd355e32867a831 100644
--- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
+++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java
@@ -180,7 +180,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..d9da03860037c727b27cdc5d0d19cb21780a385d
--- /dev/null
+++ b/briar-tests/src/org/briarproject/privategroup/GroupMessageValidatorTest.java
@@ -0,0 +1,534 @@
+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.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 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_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.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 = "Member Name";
+	private final String memberName = "Member Name";
+	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, "Private Group Name", creator,
+					getRandomBytes(GROUP_SALT_LENGTH));
+	private final BdfList token = BdfList.of("token");
+	private MessageId parentId = new MessageId(getRandomId());
+	private MessageId previousMsgId = new MessageId(getRandomId());
+	private String postContent = "Post text";
+
+	private GroupMessageValidator validator =
+			new GroupMessageValidator(privateGroupFactory, clientHelper,
+					metadataEncoder, clock, authorFactory,
+					groupInvitationFactory);
+
+	@Test(expected = FormatException.class)
+	public void testRejectTooShortMemberName() throws Exception {
+		BdfList list = BdfList.of(JOIN.getInt(), "", memberKey, null,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectTooLongMemberName() throws Exception {
+		BdfList list = BdfList.of(JOIN.getInt(),
+				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), memberKey, null,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectTooShortMemberKey() throws Exception {
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, new byte[0], null,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectTooLongMemberKey() throws Exception {
+		BdfList list = BdfList.of(JOIN.getInt(), memberName,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), null,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectNonRawMemberKey() throws Exception {
+		BdfList list =
+				BdfList.of(JOIN.getInt(), memberName, "non raw key", null,
+						signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	// JOIN message
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortJoinMessage() throws Exception {
+		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				null);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongJoinMessage() throws Exception {
+		expectCreateAuthor(creator);
+		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				null, signature, "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNonListInvitation() throws Exception {
+		expectCreateAuthor(creator);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				"not a list", signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptCreatorJoinMessage() throws Exception {
+		final BdfList invite = null;
+		expectJoinMessage(creator, invite, true, true);
+		BdfList list = BdfList.of(JOIN.getInt(), creatorName, creatorKey,
+				invite, signature);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertMessageContext(messageContext, creator);
+		assertTrue(messageContext.getDictionary()
+				.getBoolean(KEY_INITIAL_JOIN_MSG));
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsMemberJoinMessageWithoutInvitation()
+			throws Exception {
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, null,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortInvitation() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongInvitation() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature, "");
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsJoinMessageWithEqualInvitationTime()
+			throws Exception {
+		BdfList invite = BdfList.of(message.getTimestamp(), creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsJoinMessageWithLaterInvitationTime()
+			throws Exception {
+		BdfList invite =
+				BdfList.of(message.getTimestamp() + 1, creatorSignature);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithNonRawCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, "non-raw signature");
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, new byte[0]);
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp,
+				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		expectCreateAuthor(member);
+		expectParsePrivateGroup();
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsJoinMessageWithInvalidCreatorSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, false, true);
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsJoinMessageWithInvalidMemberSignature()
+			throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, true, false);
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptMemberJoinMessage() throws Exception {
+		BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
+		expectJoinMessage(member, invite, true, true);
+		BdfList list = BdfList.of(JOIN.getInt(), memberName, memberKey, invite,
+				signature);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertMessageContext(messageContext, member);
+		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 list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongPost() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature, "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawParentId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, "non-raw",
+						previousMsgId, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortParentId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey,
+						getRandomBytes(MessageId.LENGTH - 1), previousMsgId,
+						postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongParentId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey,
+						getRandomBytes(MessageId.LENGTH + 1), previousMsgId,
+						postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawPreviousMsgId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						"non-raw", postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooShortPreviousMsgId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						getRandomBytes(MessageId.LENGTH - 1),
+						postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongPreviousMsgId() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						getRandomBytes(MessageId.LENGTH + 1),
+						postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithEmptyContent() throws Exception {
+		postContent = "";
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongContent() throws Exception {
+		postContent = getRandomString(MAX_GROUP_POST_BODY_LENGTH + 1);
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonStringContent() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, getRandomBytes(5), signature);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithEmptySignature() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, new byte[0]);
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithTooLongSignature() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent,
+						getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsPostWithNonRawSignature() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, "non-raw");
+		expectCreateAuthor(member);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = InvalidMessageException.class)
+	public void testRejectsPostWithInvalidSignature() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature);
+		expectPostMessage(member, false);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptPost() throws Exception {
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature);
+		expectPostMessage(member, true);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertMessageContext(messageContext, member);
+		assertEquals(previousMsgId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
+		assertEquals(parentId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PARENT_MSG_ID));
+	}
+
+	@Test
+	public void testAcceptTopLevelPost() throws Exception {
+		parentId = null;
+		BdfList list =
+				BdfList.of(POST.getInt(), memberName, memberKey, parentId,
+						previousMsgId, postContent, signature);
+		expectPostMessage(member, true);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertMessageContext(messageContext, member);
+		assertEquals(previousMsgId.getBytes(),
+				messageContext.getDictionary().getRaw(KEY_PREVIOUS_MSG_ID));
+		assertFalse(
+				messageContext.getDictionary().containsKey(KEY_PARENT_MSG_ID));
+	}
+
+	private void expectPostMessage(final Author member, 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 == null ? null : 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 assertMessageContext(BdfMessageContext c, Author member)
+			throws FormatException {
+		BdfDictionary d = c.getDictionary();
+		assertTrue(message.getTimestamp() == d.getLong(KEY_TIMESTAMP));
+		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));
+
+		// assert message dependencies
+		if (d.getLong(KEY_TYPE) == POST.getInt()) {
+			assertTrue(c.getDependencies().contains(previousMsgId));
+			if (parentId != null) {
+				assertTrue(c.getDependencies().contains(parentId));
+			} else {
+				assertFalse(c.getDependencies().contains(parentId));
+			}
+		} else {
+			assertEquals(JOIN.getInt(), d.getLong(KEY_TYPE).intValue());
+		}
+	}
+
+}
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..093de60f3300d223599b61255eb95a3d650c00e9
--- /dev/null
+++ b/briar-tests/src/org/briarproject/privategroup/invitation/GroupInvitationValidatorTest.java
@@ -0,0 +1,424 @@
+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 = "Group Name";
+	private final String creatorName = "Creator Name";
+	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 = "Invitation Text";
+	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 GroupInvitationValidator validator =
+			new GroupInvitationValidator(clientHelper, metadataEncoder,
+					clock, authorFactory, privateGroupFactory, messageEncoder);
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooShortInviteMessage() throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongInviteMessage() throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, signature, "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongGroupName()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(),
+				getRandomString(MAX_GROUP_NAME_LENGTH + 1), creatorName,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithEmptyGroupName()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), "", creatorName,
+				creatorKey, salt, inviteText, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongCreatorName()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName,
+				getRandomString(MAX_AUTHOR_NAME_LENGTH + 1), creatorKey, salt,
+				inviteText, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithEmptyCreatorName()
+			throws Exception {
+		BdfList list =
+				BdfList.of(INVITE.getValue(), groupName, "", creatorKey, salt,
+						inviteText, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongCreatorKey()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), salt, inviteText,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithEmptyCreatorKey()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				new byte[0], salt, inviteText, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongGroupSalt()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, getRandomBytes(GROUP_SALT_LENGTH + 1), inviteText,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooShortGroupSalt()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, getRandomBytes(GROUP_SALT_LENGTH - 1), inviteText,
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongMessage()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt,
+				getRandomString(MAX_GROUP_INVITATION_MSG_LENGTH + 1),
+				signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithTooLongSignature()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText,
+				getRandomBytes(MAX_SIGNATURE_LENGTH + 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithEmptySignature()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, new byte[0]);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNullSignature()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, null);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithNonRawSignature()
+			throws Exception {
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, "non raw signature");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptsInviteMessageWithNullMessage()
+			throws Exception {
+		expectInviteMessage(false);
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, null, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsInviteMessageWithInvalidSignature()
+			throws Exception {
+		expectInviteMessage(true);
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, null, signature);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptsProperInviteMessage()
+			throws Exception {
+		expectInviteMessage(false);
+		BdfList list = BdfList.of(INVITE.getValue(), groupName, creatorName,
+				creatorKey, salt, inviteText, signature);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertTrue(messageContext.getDependencies().isEmpty());
+		assertEquals(meta ,messageContext.getDictionary());
+	}
+
+	private void expectInviteMessage(final boolean exception) throws Exception {
+		final BdfList toSign =
+				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, toSign);
+			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 list = BdfList.of(JOIN.getValue(), privateGroup.getId());
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongJoinMessage() throws Exception {
+		BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				previousMessageId, "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongGroupId() throws Exception {
+		BdfList list =
+				BdfList.of(JOIN.getValue(), getRandomBytes(GroupId.LENGTH + 1),
+						previousMessageId);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortGroupId() throws Exception {
+		BdfList list =
+				BdfList.of(JOIN.getValue(), getRandomBytes(GroupId.LENGTH - 1),
+						previousMessageId);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooLongPreviousMessageId()
+			throws Exception {
+		BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH + 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsJoinMessageWithTooShortPreviousMessageId()
+			throws Exception {
+		BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH - 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptsProperJoinMessage()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder)
+					.encodeMetadata(JOIN, message.getGroupId(),
+							message.getTimestamp(), false, false, false,
+							false);
+			will(returnValue(meta));
+		}});
+		BdfList list = BdfList.of(JOIN.getValue(), privateGroup.getId(),
+				previousMessageId);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		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 list = BdfList.of(LEAVE.getValue(), privateGroup.getId());
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongLeaveMessage() throws Exception {
+		BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				previousMessageId, "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooLongGroupId() throws Exception {
+		BdfList list =
+				BdfList.of(LEAVE.getValue(), getRandomBytes(GroupId.LENGTH + 1),
+						previousMessageId);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooShortGroupId() throws Exception {
+		BdfList list =
+				BdfList.of(LEAVE.getValue(), getRandomBytes(GroupId.LENGTH - 1),
+						previousMessageId);
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooLongPreviousMessageId()
+			throws Exception {
+		BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH + 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsLeaveMessageWithTooShortPreviousMessageId()
+			throws Exception {
+		BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				getRandomBytes(UniqueId.LENGTH - 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptsProperLeaveMessage()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(LEAVE, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfList list = BdfList.of(LEAVE.getValue(), privateGroup.getId(),
+				previousMessageId);
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		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 list = BdfList.of(ABORT.getValue());
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsTooLongAbortMessage() throws Exception {
+		BdfList list = BdfList.of(ABORT.getValue(), privateGroup.getId(), "");
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithTooLongGroupId() throws Exception {
+		BdfList list = BdfList.of(ABORT.getValue(),
+				getRandomBytes(GroupId.LENGTH + 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsAbortMessageWithTooShortGroupId() throws Exception {
+		BdfList list = BdfList.of(ABORT.getValue(),
+				getRandomBytes(GroupId.LENGTH - 1));
+		validator.validateMessage(message, group, list);
+	}
+
+	@Test
+	public void testAcceptsProperAbortMessage()
+			throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder).encodeMetadata(ABORT, message.getGroupId(),
+					message.getTimestamp(), false, false, false, false);
+			will(returnValue(meta));
+		}});
+		BdfList list = BdfList.of(ABORT.getValue(), privateGroup.getId());
+		BdfMessageContext messageContext =
+				validator.validateMessage(message, group, list);
+		assertEquals(0, messageContext.getDependencies().size());
+		assertEquals(meta ,messageContext.getDictionary());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testRejectsMessageWithUnknownType() throws Exception {
+		BdfList list = BdfList.of(ABORT.getValue() + 1);
+		validator.validateMessage(message, group, list);
+	}
+
+}