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 fedaa929a787c8c6e14fb27f090ab9ccf9e89196..198a3039d65b155f54d23429b1bd2048a9f34253 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 5be3b4573143e41cb97234ffbebcba73e9ca8723..6873a910dbe705aba397ab1fab3444bbd0f954b4 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 fe03fdb7c6ac5207fa9916905b7c28137b798b63..231af013511dc8b8dd2df537266948ae4e8094ea 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