From d51f73151f22a91bfa4d548f60edce5d5276beb7 Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Wed, 30 Nov 2016 11:12:04 +0000
Subject: [PATCH] Fixed a bug in the group invitation protocol, added tests.

---
 .../GroupInvitationIntegrationTest.java       | 111 +++++++++++++++++-
 .../GroupInvitationManagerImpl.java           |   2 +-
 .../invitation/InviteeProtocolEngine.java     |  24 +++-
 3 files changed, 130 insertions(+), 7 deletions(-)

diff --git a/briar-android-tests/src/test/java/org/briarproject/GroupInvitationIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/GroupInvitationIntegrationTest.java
index fedaa929a7..198a3039d6 100644
--- a/briar-android-tests/src/test/java/org/briarproject/GroupInvitationIntegrationTest.java
+++ b/briar-android-tests/src/test/java/org/briarproject/GroupInvitationIntegrationTest.java
@@ -265,8 +265,31 @@ public class GroupInvitationIntegrationTest extends BriarIntegrationTest {
 				.respondToInvitation(contactId0From1, privateGroup0, true);
 	}
 
+	@Test(expected = ProtocolStateException.class)
+	public void testCreatorLeavesBeforeInvitationAccepted() throws Exception {
+		// Creator invites invitee to join group
+		sendInvitation(clock.currentTimeMillis(), null);
+
+		// Creator's invite message is delivered to invitee
+		sync0To1(1, true);
+
+		// Creator leaves group
+		assertEquals(1, groupManager0.getPrivateGroups().size());
+		groupManager0.removePrivateGroup(privateGroup0.getId());
+		assertEquals(0, groupManager0.getPrivateGroups().size());
+
+		// Creator's leave message is delivered to invitee
+		sync0To1(1, true);
+
+		// Invitee accepts invitation, but it's no longer open - exception is
+		// thrown as the action has failed
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+		groupInvitationManager1
+				.respondToInvitation(contactId0From1, privateGroup0, true);
+	}
+
 	@Test
-	public void testCreatorLeavesBeforeInvitationAnswered() throws Exception {
+	public void testCreatorLeavesBeforeInvitationDeclined() throws Exception {
 		// Creator invites invitee to join group
 		sendInvitation(clock.currentTimeMillis(), null);
 
@@ -278,7 +301,31 @@ public class GroupInvitationIntegrationTest extends BriarIntegrationTest {
 		groupManager0.removePrivateGroup(privateGroup0.getId());
 		assertEquals(0, groupManager0.getPrivateGroups().size());
 
-		// Invitee responds to invitation
+		// Creator's leave message is delivered to invitee
+		sync0To1(1, true);
+
+		// Invitee declines invitation, but it's no longer open - no exception
+		// as the action has succeeded
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+		groupInvitationManager1
+				.respondToInvitation(contactId0From1, privateGroup0, false);
+	}
+
+	@Test
+	public void testCreatorLeavesConcurrentlyWithInvitationAccepted()
+			throws Exception {
+		// Creator invites invitee to join group
+		sendInvitation(clock.currentTimeMillis(), null);
+
+		// Creator's invite message is delivered to invitee
+		sync0To1(1, true);
+
+		// Creator leaves group
+		assertEquals(1, groupManager0.getPrivateGroups().size());
+		groupManager0.removePrivateGroup(privateGroup0.getId());
+		assertEquals(0, groupManager0.getPrivateGroups().size());
+
+		// Invitee accepts invitation
 		assertEquals(0, groupManager1.getPrivateGroups().size());
 		groupInvitationManager1
 				.respondToInvitation(contactId0From1, privateGroup0, true);
@@ -293,7 +340,67 @@ public class GroupInvitationIntegrationTest extends BriarIntegrationTest {
 
 		// Group is marked as dissolved
 		assertTrue(groupManager1.isDissolved(privateGroup0.getId()));
+	}
+
+	@Test
+	public void testCreatorLeavesConcurrentlyWithInvitationDeclined()
+			throws Exception {
+		// Creator invites invitee to join group
+		sendInvitation(clock.currentTimeMillis(), null);
 
+		// Creator's invite message is delivered to invitee
+		sync0To1(1, true);
+
+		// Creator leaves group
+		assertEquals(1, groupManager0.getPrivateGroups().size());
+		groupManager0.removePrivateGroup(privateGroup0.getId());
+		assertEquals(0, groupManager0.getPrivateGroups().size());
+
+		// Invitee declines invitation
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+		groupInvitationManager1
+				.respondToInvitation(contactId0From1, privateGroup0, false);
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+
+		// Invitee's leave message is delivered to creator
+		sync1To0(1, true);
+
+		// Creator's leave message is delivered to invitee
+		sync0To1(1, true);
+	}
+
+	@Test
+	public void testCreatorLeavesConcurrentlyWithMemberLeaving()
+			throws Exception {
+		// Creator invites invitee to join group
+		sendInvitation(clock.currentTimeMillis(), null);
+
+		// Creator's invite message is delivered to invitee
+		sync0To1(1, true);
+
+		// Invitee responds to invitation
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+		groupInvitationManager1
+				.respondToInvitation(contactId0From1, privateGroup0, true);
+		assertEquals(1, groupManager1.getPrivateGroups().size());
+
+		// Invitee's join message is delivered to creator
+		sync1To0(1, true);
+
+		// Creator leaves group
+		assertEquals(1, groupManager0.getPrivateGroups().size());
+		groupManager0.removePrivateGroup(privateGroup0.getId());
+		assertEquals(0, groupManager0.getPrivateGroups().size());
+
+		// Invitee leaves group
+		groupManager1.removePrivateGroup(privateGroup0.getId());
+		assertEquals(0, groupManager1.getPrivateGroups().size());
+
+		// Creator's leave message is delivered to invitee
+		sync0To1(1, true);
+
+		// Invitee's leave message is delivered to creator
+		sync1To0(1, true);
 	}
 
 	private void sendInvitation(long timestamp, @Nullable String msg) throws
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
index 5be3b45731..6873a910db 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java
@@ -74,7 +74,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl
 	private final Group localGroup;
 
 	@Inject
-	protected GroupInvitationManagerImpl(DatabaseComponent db,
+	GroupInvitationManagerImpl(DatabaseComponent db,
 			ClientHelper clientHelper, MetadataParser metadataParser,
 			MessageTracker messageTracker,
 			ContactGroupFactory contactGroupFactory,
diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
index fe03fdb7c6..231af01351 100644
--- a/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
+++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java
@@ -148,10 +148,11 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 			case DISSOLVED:
 				return abort(txn, s); // Invalid in these states
 			case INVITED:
+			case LEFT:
+				return onRemoteLeaveWhenNotSubscribed(txn, s, m);
 			case ACCEPTED:
 			case JOINED:
-			case LEFT:
-				return onRemoteLeave(txn, s, m);
+				return onRemoteLeaveWhenSubscribed(txn, s, m);
 			case ERROR:
 				return s; // Ignored in this state
 			default:
@@ -260,8 +261,23 @@ class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
 				s.getInviteTimestamp(), JOINED);
 	}
 
-	private InviteeSession onRemoteLeave(Transaction txn, InviteeSession s,
-			LeaveMessage m) throws DbException, FormatException {
+	private InviteeSession onRemoteLeaveWhenNotSubscribed(Transaction txn,
+			InviteeSession s, LeaveMessage m)
+			throws DbException, FormatException {
+		// The timestamp must be higher than the last invite message, if any
+		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
+		// The dependency, if any, must be the last remote message
+		if (!isValidDependency(s, m.getPreviousMessageId()))
+			return abort(txn, s);
+		// Move to the DISSOLVED state
+		return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
+				s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
+				s.getInviteTimestamp(), DISSOLVED);
+	}
+
+	private InviteeSession onRemoteLeaveWhenSubscribed(Transaction txn,
+			InviteeSession s, LeaveMessage m)
+			throws DbException, FormatException {
 		// The timestamp must be higher than the last invite message, if any
 		if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
 		// The dependency, if any, must be the last remote message
-- 
GitLab