diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
index 7be98da26b1c6e09245272af845eeaee37bf00a3..14853dfb186df358fa232b8958aca2ebe3c25639 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/ForumSharingValidator.java
@@ -1,89 +1,47 @@
 package org.briarproject.briar.sharing;
 
 import org.briarproject.bramble.api.FormatException;
-import org.briarproject.bramble.api.client.BdfMessageContext;
 import org.briarproject.bramble.api.client.ClientHelper;
-import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.system.Clock;
-import org.briarproject.briar.api.client.SessionId;
-import org.briarproject.briar.client.BdfQueueMessageValidator;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
 
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.bramble.util.ValidationUtils.checkLength;
 import static org.briarproject.bramble.util.ValidationUtils.checkSize;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
 import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_MSG;
-import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
-import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
-import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
 
 @Immutable
 @NotNullByDefault
-class ForumSharingValidator extends BdfQueueMessageValidator {
+class ForumSharingValidator extends SharingValidator {
+
+	private final ForumFactory forumFactory;
 
 	@Inject
-	ForumSharingValidator(ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-		super(clientHelper, metadataEncoder, clock);
+	ForumSharingValidator(MessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock, ForumFactory forumFactory) {
+		super(messageEncoder, clientHelper, metadataEncoder, clock);
+		this.forumFactory = forumFactory;
 	}
 
 	@Override
-	protected BdfMessageContext validateMessage(Message m, Group g,
-			BdfList body) throws FormatException {
-
-		BdfDictionary d = new BdfDictionary();
-		long type = body.getLong(0);
-		byte[] id = body.getRaw(1);
-		checkLength(id, SessionId.LENGTH);
-
-		if (type == SHARE_MSG_TYPE_INVITATION) {
-			checkSize(body, 4, 5);
-
-			String name = body.getString(2);
-			checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
-
-			byte[] salt = body.getRaw(3);
-			checkLength(salt, FORUM_SALT_LENGTH);
-
-			d.put(FORUM_NAME, name);
-			d.put(FORUM_SALT, salt);
-
-			if (body.size() > 4) {
-				String msg = body.getString(4);
-				checkLength(msg, 0, MAX_INVITATION_MESSAGE_LENGTH);
-				d.put(INVITATION_MSG, msg);
-			}
-		} else {
-			checkSize(body, 2);
-			if (type != SHARE_MSG_TYPE_ACCEPT &&
-					type != SHARE_MSG_TYPE_DECLINE &&
-					type != SHARE_MSG_TYPE_LEAVE &&
-					type != SHARE_MSG_TYPE_ABORT) {
-				throw new FormatException();
-			}
-		}
-		// Return the metadata
-		d.put(TYPE, type);
-		d.put(SESSION_ID, id);
-		d.put(LOCAL, false);
-		d.put(TIME, m.getTimestamp());
-		return new BdfMessageContext(d);
+	protected GroupId validateDescriptor(BdfList descriptor)
+			throws FormatException {
+		checkSize(descriptor, 2);
+		String name = descriptor.getString(0);
+		checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
+		byte[] salt = descriptor.getRaw(1);
+		checkLength(salt, FORUM_SALT_LENGTH);
+		Forum forum = forumFactory.createForum(name, salt);
+		return forum.getId();
 	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..d19d55bdfa28491247e7fce29db3624d1dd859e9
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoder.java
@@ -0,0 +1,39 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface MessageEncoder {
+
+	BdfDictionary encodeMetadata(MessageType type, GroupId groupId,
+			long timestamp, boolean local, boolean read, boolean visible,
+			boolean available);
+
+	void setVisibleInUi(BdfDictionary meta, boolean visible);
+
+	void setAvailableToAnswer(BdfDictionary meta, boolean available);
+
+	Message encodeInviteMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, BdfList descriptor,
+			@Nullable String message);
+
+	Message encodeAcceptMessage(GroupId contactGroupId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeDeclineMessage(GroupId contactGroupId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeLeaveMessage(GroupId contactGroupId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+	Message encodeAbortMessage(GroupId contactGroupId, GroupId groupId,
+			long timestamp, @Nullable MessageId previousMessageId);
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..90fa15b9f758f7271b95f04742ba1ac042aace24
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageEncoderImpl.java
@@ -0,0 +1,135 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageFactory;
+import org.briarproject.bramble.api.sync.MessageId;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_LOCAL;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_MESSAGE_TYPE;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_PRIVATE_GROUP_ID;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_READ;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_TIMESTAMP;
+import static org.briarproject.briar.sharing.SharingConstants.MSG_KEY_VISIBLE_IN_UI;
+
+@Immutable
+@NotNullByDefault
+class MessageEncoderImpl implements MessageEncoder {
+
+	private final ClientHelper clientHelper;
+	private final MessageFactory messageFactory;
+
+	@Inject
+	MessageEncoderImpl(ClientHelper clientHelper,
+			MessageFactory messageFactory) {
+		this.clientHelper = clientHelper;
+		this.messageFactory = messageFactory;
+	}
+
+	@Override
+	public BdfDictionary encodeMetadata(MessageType type,
+			GroupId groupId, long timestamp, boolean local, boolean read,
+			boolean visible, boolean available) {
+		BdfDictionary meta = new BdfDictionary();
+		meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
+		meta.put(MSG_KEY_PRIVATE_GROUP_ID, groupId);
+		meta.put(MSG_KEY_TIMESTAMP, timestamp);
+		meta.put(MSG_KEY_LOCAL, local);
+		meta.put(MSG_KEY_READ, read);
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+		return meta;
+	}
+
+	@Override
+	public void setVisibleInUi(BdfDictionary meta, boolean visible) {
+		meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
+	}
+
+	@Override
+	public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
+		meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
+	}
+
+	@Override
+	public Message encodeInviteMessage(GroupId contactGroupId, long timestamp,
+			@Nullable MessageId previousMessageId, BdfList descriptor,
+			@Nullable String message) {
+		BdfList body = BdfList.of(
+				INVITE.getValue(),
+				previousMessageId,
+				descriptor,
+				message
+		);
+		try {
+			return messageFactory.createMessage(contactGroupId, timestamp,
+					clientHelper.toByteArray(body));
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	@Override
+	public Message encodeAcceptMessage(GroupId contactGroupId,
+			GroupId privateGroupId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(ACCEPT, contactGroupId, privateGroupId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeDeclineMessage(GroupId contactGroupId,
+			GroupId privateGroupId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(DECLINE, contactGroupId, privateGroupId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeLeaveMessage(GroupId contactGroupId,
+			GroupId privateGroupId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(LEAVE, contactGroupId, privateGroupId, timestamp,
+				previousMessageId);
+	}
+
+	@Override
+	public Message encodeAbortMessage(GroupId contactGroupId,
+			GroupId privateGroupId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		return encodeMessage(ABORT, contactGroupId, privateGroupId, timestamp,
+				previousMessageId);
+	}
+
+	private Message encodeMessage(MessageType type, GroupId contactGroupId,
+			GroupId groupId, long timestamp,
+			@Nullable MessageId previousMessageId) {
+		BdfList body = BdfList.of(
+				type.getValue(),
+				groupId,
+				previousMessageId
+		);
+		try {
+			return messageFactory.createMessage(contactGroupId, timestamp,
+					clientHelper.toByteArray(body));
+		} catch (FormatException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java
new file mode 100644
index 0000000000000000000000000000000000000000..1af500a745d73bd0178c44551bc8f88a7fef6e65
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/MessageType.java
@@ -0,0 +1,29 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum MessageType {
+
+	INVITE(0), ACCEPT(1), DECLINE(2), LEAVE(3), ABORT(4);
+
+	private final int value;
+
+	MessageType(int value) {
+		this.value = value;
+	}
+
+	int getValue() {
+		return value;
+	}
+
+	static MessageType fromValue(int value) throws FormatException {
+		for (MessageType m : values()) if (m.value == value) return m;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae3edc982f8b65cabca55253ee17dd925fa3cac5
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingConstants.java
@@ -0,0 +1,29 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.briar.client.MessageTrackerConstants;
+
+interface SharingConstants {
+
+	// Group metadata keys
+	String GROUP_KEY_CONTACT_ID = "contactId";
+
+	// Message metadata keys
+	String MSG_KEY_MESSAGE_TYPE = "messageType";
+	String MSG_KEY_PRIVATE_GROUP_ID = "privateGroupId";
+	String MSG_KEY_TIMESTAMP = "timestamp";
+	String MSG_KEY_READ = MessageTrackerConstants.MSG_KEY_READ;
+	String MSG_KEY_LOCAL = "local";
+	String MSG_KEY_VISIBLE_IN_UI = "visibleInUi";
+	String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
+
+	// Session keys
+	String SESSION_KEY_SESSION_ID = "sessionId";
+	String SESSION_KEY_GROUP_ID = "groupId";
+	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
+	String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
+	String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
+	String SESSION_KEY_INVITE_TIMESTAMP = "inviteTimestamp";
+	String SESSION_KEY_ROLE = "role";
+	String SESSION_KEY_STATE = "state";
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
index f56e6b8ca88b99279002853e3508a9390ac8673e..9a6e4de732b55be92913eaf304c8e78061bb9237 100644
--- a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingModule.java
@@ -4,10 +4,12 @@ import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.data.MetadataEncoder;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.sync.ValidationManager;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.blog.BlogManager;
 import org.briarproject.briar.api.blog.BlogSharingManager;
 import org.briarproject.briar.api.client.MessageQueueManager;
+import org.briarproject.briar.api.forum.ForumFactory;
 import org.briarproject.briar.api.forum.ForumManager;
 import org.briarproject.briar.api.forum.ForumSharingManager;
 import org.briarproject.briar.api.messaging.ConversationManager;
@@ -68,14 +70,15 @@ public class SharingModule {
 	@Provides
 	@Singleton
 	ForumSharingValidator provideForumSharingValidator(
-			MessageQueueManager messageQueueManager, ClientHelper clientHelper,
-			MetadataEncoder metadataEncoder, Clock clock) {
-
+			ValidationManager validationManager, MessageEncoder messageEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock, ForumFactory forumFactory) {
 		ForumSharingValidator validator =
-				new ForumSharingValidator(clientHelper, metadataEncoder, clock);
-		messageQueueManager.registerMessageValidator(
-				ForumSharingManager.CLIENT_ID, validator);
-
+				new ForumSharingValidator(messageEncoder, clientHelper,
+						metadataEncoder, clock, forumFactory);
+		validationManager
+				.registerMessageValidator(ForumSharingManager.CLIENT_ID,
+						validator);
 		return validator;
 	}
 
@@ -98,4 +101,9 @@ public class SharingModule {
 		return forumSharingManager;
 	}
 
+	@Provides
+	MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
+		return messageEncoder;
+	}
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..631a6b30c7817e9edb279bd510e085bcd312bb91
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/sharing/SharingValidator.java
@@ -0,0 +1,101 @@
+package org.briarproject.briar.sharing;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.UniqueId;
+import org.briarproject.bramble.api.client.BdfMessageContext;
+import org.briarproject.bramble.api.client.BdfMessageValidator;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.data.MetadataEncoder;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.system.Clock;
+
+import java.util.Collections;
+
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.bramble.util.ValidationUtils.checkLength;
+import static org.briarproject.bramble.util.ValidationUtils.checkSize;
+import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+
+@Immutable
+@NotNullByDefault
+abstract class SharingValidator extends BdfMessageValidator {
+
+	private final MessageEncoder messageEncoder;
+
+	SharingValidator(MessageEncoder messageEncoder, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+		this.messageEncoder = messageEncoder;
+	}
+
+	@Override
+	protected BdfMessageContext validateMessage(Message m, Group g,
+			BdfList body) throws FormatException {
+		MessageType type = MessageType.fromValue(body.getLong(0).intValue());
+		switch (type) {
+			case INVITE:
+				return validateInviteMessage(m, body);
+			case ACCEPT:
+			case DECLINE:
+			case LEAVE:
+			case ABORT:
+				return validateNonInviteMessage(type, m, body);
+			default:
+				throw new FormatException();
+		}
+	}
+
+	private BdfMessageContext validateInviteMessage(Message m, BdfList body)
+			throws FormatException {
+		checkSize(body, 4);
+		byte[] previousMessageId = body.getOptionalRaw(1);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+		BdfList descriptor = body.getList(2);
+		GroupId groupId = validateDescriptor(descriptor);
+		String msg = body.getOptionalString(3);
+		checkLength(msg, 1, MAX_INVITATION_MESSAGE_LENGTH);
+
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(INVITE, groupId, m.getTimestamp(), false, false,
+						false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+	protected abstract GroupId validateDescriptor(BdfList descriptor)
+			throws FormatException;
+
+	private BdfMessageContext validateNonInviteMessage(MessageType type,
+			Message m, BdfList body) throws FormatException {
+		checkSize(body, 3);
+		byte[] groupId = body.getRaw(1);
+		checkLength(groupId, UniqueId.LENGTH);
+		byte[] previousMessageId = body.getOptionalRaw(2);
+		checkLength(previousMessageId, UniqueId.LENGTH);
+
+		BdfDictionary meta = messageEncoder
+				.encodeMetadata(type, new GroupId(groupId), m.getTimestamp(),
+						false, false, false, false);
+		if (previousMessageId == null) {
+			return new BdfMessageContext(meta);
+		} else {
+			MessageId dependency = new MessageId(previousMessageId);
+			return new BdfMessageContext(meta,
+					Collections.singletonList(dependency));
+		}
+	}
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
index 52887134cf3c0945aac3d2ef02a4d5bc1bdfb7b2..3772e87fc52eb281d29181351e8877dc70b14386 100644
--- a/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/sharing/ForumSharingValidatorTest.java
@@ -3,341 +3,310 @@ package org.briarproject.briar.sharing;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.UniqueId;
 import org.briarproject.bramble.api.client.BdfMessageContext;
-import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.data.BdfList;
+import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.test.TestUtils;
 import org.briarproject.bramble.test.ValidatorTestCase;
-import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.forum.Forum;
+import org.briarproject.briar.api.forum.ForumFactory;
+import org.jmock.Expectations;
 import org.junit.Test;
 
+import java.util.Collection;
+
 import javax.annotation.Nullable;
 
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_NAME;
-import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
 import static org.briarproject.briar.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.briar.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.INVITATION_MSG;
-import static org.briarproject.briar.api.sharing.SharingConstants.LOCAL;
 import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_MESSAGE_LENGTH;
-import static org.briarproject.briar.api.sharing.SharingConstants.SESSION_ID;
 import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ABORT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_ACCEPT;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_DECLINE;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_INVITATION;
-import static org.briarproject.briar.api.sharing.SharingConstants.SHARE_MSG_TYPE_LEAVE;
-import static org.briarproject.briar.api.sharing.SharingConstants.TIME;
-import static org.briarproject.briar.api.sharing.SharingConstants.TYPE;
+import static org.briarproject.briar.sharing.MessageType.ABORT;
+import static org.briarproject.briar.sharing.MessageType.ACCEPT;
+import static org.briarproject.briar.sharing.MessageType.DECLINE;
+import static org.briarproject.briar.sharing.MessageType.INVITE;
+import static org.briarproject.briar.sharing.MessageType.LEAVE;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 public class ForumSharingValidatorTest extends ValidatorTestCase {
 
-	private final SessionId sessionId = new SessionId(TestUtils.getRandomId());
+	private final MessageEncoder messageEncoder =
+			context.mock(MessageEncoder.class);
+	private final ForumFactory forumFactory = context.mock(ForumFactory.class);
+	private final ForumSharingValidator v =
+			new ForumSharingValidator(messageEncoder, clientHelper,
+					metadataEncoder, clock, forumFactory);
+
+	private final MessageId previousMsgId = new MessageId(getRandomId());
 	private final String forumName =
 			TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH);
 	private final byte[] salt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH);
+	private final Forum forum = new Forum(group, forumName, salt);
+	private final BdfList descriptor = BdfList.of(forumName, salt);
 	private final String content =
 			TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH);
 
 	@Test
 	public void testAcceptsInvitationWithContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
+		BdfMessageContext messageContext = v.validateMessage(message, group,
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						content));
+		assertExpectedContext(messageContext, previousMsgId);
+	}
+
+	@Test
+	public void testAcceptsInvitationWithNullContent() throws Exception {
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, content));
-		assertExpectedContextForInvitation(messageContext, forumName, content);
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
-	public void testAcceptsInvitationWithoutContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+	public void testAcceptsInvitationWithNullPreviousMsgId() throws Exception {
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt));
-		assertExpectedContextForInvitation(messageContext, forumName, null);
+				BdfList.of(INVITE.getValue(), null, descriptor, null));
+		assertExpectedContext(messageContext, null);
 	}
 
 	@Test
 	public void testAcceptsAccept() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(ACCEPT);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ACCEPT, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ACCEPT);
+				BdfList.of(ACCEPT.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsDecline() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(DECLINE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_DECLINE, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_DECLINE);
+				BdfList.of(DECLINE.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsLeave() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(LEAVE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_LEAVE, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_LEAVE);
+				BdfList.of(LEAVE.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test
 	public void testAcceptsAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectEncodeMetadata(ABORT);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId));
-		assertExpectedContext(messageContext, SHARE_MSG_TYPE_ABORT);
+				BdfList.of(ABORT.getValue(), groupId, previousMsgId));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullMessageType() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of(null, sessionId));
+		v.validateMessage(message, group,
+				BdfList.of(null, groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonLongMessageType() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of("", sessionId));
+		v.validateMessage(message, group,
+				BdfList.of("", groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsInvalidMessageType() throws Exception {
 		int invalidMessageType = SHARE_MSG_TYPE_ABORT + 1;
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(invalidMessageType, sessionId));
+				BdfList.of(invalidMessageType, groupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
-	public void testRejectsNullSessionId() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+	public void testRejectsNullGroupId() throws Exception {
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, null));
+				BdfList.of(ABORT.getValue(), null, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonRawSessionId() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
 				BdfList.of(SHARE_MSG_TYPE_ABORT, 123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortSessionId() throws Exception {
-		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH - 1);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+				BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongSessionId() throws Exception {
-		byte[] invalidSessionId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		byte[] invalidGroupId = TestUtils.getRandomBytes(UniqueId.LENGTH + 1);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, invalidSessionId));
+				BdfList.of(ABORT.getValue(), invalidGroupId, previousMsgId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortBodyForAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group, BdfList.of(SHARE_MSG_TYPE_ABORT));
+		v.validateMessage(message, group,
+				BdfList.of(ABORT.getValue(), groupId));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongBodyForAbort() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_ABORT, sessionId, 123));
+				BdfList.of(SHARE_MSG_TYPE_ABORT, groupId, previousMsgId, 123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortBodyForInvitation() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName));
+				BdfList.of(INVITE.getValue(), groupId, forumName));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongBodyForInvitation() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, content, 123));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, null,
+						123));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(null, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, null,
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonStringForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(123, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, 123,
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortForumName() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of("", salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, "",
-						salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test
 	public void testAcceptsMinLengthForumName() throws Exception {
 		String shortForumName = TestUtils.getRandomString(1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, shortForumName,
-						salt, content));
-		assertExpectedContextForInvitation(messageContext, shortForumName,
-				content);
+		BdfList validDescriptor = BdfList.of(shortForumName, salt);
+		expectCreateForum(shortForumName);
+		expectEncodeMetadata(INVITE);
+		v.validateMessage(message, group,
+				BdfList.of(INVITE.getValue(), previousMsgId, validDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongForumName() throws Exception {
 		String invalidForumName =
 				TestUtils.getRandomString(MAX_FORUM_NAME_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(invalidForumName, salt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId,
-						invalidForumName, salt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNullSalt() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, null);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						null, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonRawSalt() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, 123);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						123, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooShortSalt() throws Exception {
 		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH - 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						invalidSalt, content));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongSalt() throws Exception {
 		byte[] invalidSalt = TestUtils.getRandomBytes(FORUM_SALT_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		BdfList invalidDescriptor = BdfList.of(forumName, invalidSalt);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						invalidSalt, content));
-	}
-
-	@Test(expected = FormatException.class)
-	public void testRejectsNullContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
-		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, null));
+				BdfList.of(INVITE.getValue(), previousMsgId, invalidDescriptor,
+						null));
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsNonStringContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, 123));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						123));
 	}
 
 	@Test
 	public void testAcceptsMinLengthContent() throws Exception {
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
+		expectEncodeMetadata(INVITE);
 		BdfMessageContext messageContext = v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, ""));
-		assertExpectedContextForInvitation(messageContext, forumName, "");
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor, "1"));
+		assertExpectedContext(messageContext, previousMsgId);
 	}
 
 	@Test(expected = FormatException.class)
 	public void testRejectsTooLongContent() throws Exception {
 		String invalidContent =
 				TestUtils.getRandomString(MAX_INVITATION_MESSAGE_LENGTH + 1);
-		ForumSharingValidator v = new ForumSharingValidator(clientHelper,
-				metadataEncoder, clock);
+		expectCreateForum(forumName);
 		v.validateMessage(message, group,
-				BdfList.of(SHARE_MSG_TYPE_INVITATION, sessionId, forumName,
-						salt, invalidContent));
+				BdfList.of(INVITE.getValue(), previousMsgId, descriptor,
+						invalidContent));
 	}
 
-	private void assertExpectedContextForInvitation(
-			BdfMessageContext messageContext, String forumName,
-			@Nullable String content) throws FormatException {
-		BdfDictionary meta = messageContext.getDictionary();
-		if (content == null) {
-			assertEquals(6, meta.size());
-		} else {
-			assertEquals(7, meta.size());
-			assertEquals(content, meta.getString(INVITATION_MSG));
-		}
-		assertEquals(forumName, meta.getString(FORUM_NAME));
-		assertEquals(salt, meta.getRaw(FORUM_SALT));
-		assertEquals(SHARE_MSG_TYPE_INVITATION, meta.getLong(TYPE).intValue());
-		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
-		assertFalse(meta.getBoolean(LOCAL));
-		assertEquals(timestamp, meta.getLong(TIME).longValue());
-		assertEquals(0, messageContext.getDependencies().size());
+	private void expectCreateForum(final String name) {
+		context.checking(new Expectations() {{
+			oneOf(forumFactory).createForum(name, salt);
+			will(returnValue(forum));
+		}});
+	}
+
+	private void expectEncodeMetadata(final MessageType type) {
+		context.checking(new Expectations() {{
+			oneOf(messageEncoder)
+					.encodeMetadata(type, groupId, timestamp, false, false,
+							false, false);
+		}});
 	}
 
 	private void assertExpectedContext(BdfMessageContext messageContext,
-			int type) throws FormatException {
-		BdfDictionary meta = messageContext.getDictionary();
-		assertEquals(4, meta.size());
-		assertEquals(type, meta.getLong(TYPE).intValue());
-		assertEquals(sessionId.getBytes(), meta.getRaw(SESSION_ID));
-		assertFalse(meta.getBoolean(LOCAL));
-		assertEquals(timestamp, meta.getLong(TIME).longValue());
-		assertEquals(0, messageContext.getDependencies().size());
+			@Nullable MessageId previousMsgId) throws FormatException {
+		Collection<MessageId> dependencies = messageContext.getDependencies();
+		if (previousMsgId == null) {
+			assertTrue(dependencies.isEmpty());
+		} else {
+			assertEquals(1, dependencies.size());
+			assertTrue(dependencies.contains(previousMsgId));
+		}
 	}
+
 }