From e36f275be7203f3763233f79752c02a755c454ad Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Mar 2021 13:57:23 -0300 Subject: [PATCH 1/6] Auto-delete PrivateGroup invitations and responses as well --- .../invitation/AbstractProtocolEngine.java | 12 +++ .../GroupInvitationManagerImpl.java | 75 +++++++++++++++++-- .../invitation/GroupInvitationModule.java | 6 +- .../GroupInvitationManagerImplTest.java | 17 ++--- 4 files changed, 93 insertions(+), 17 deletions(-) diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java index b52156ecd..2bf722db0 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java @@ -132,6 +132,10 @@ abstract class AbstractProtocolEngine> privateGroup.getCreator(), privateGroup.getSalt(), text, signature, timer); sendMessage(txn, m, INVITE, privateGroup.getId(), true, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeInviteMessage(s.getContactGroupId(), privateGroup.getId(), timestamp, privateGroup.getName(), @@ -162,6 +166,10 @@ abstract class AbstractProtocolEngine> s.getLastLocalMessageId(), timer); sendMessage(txn, m, JOIN, s.getPrivateGroupId(), visibleInUi, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeJoinMessage(s.getContactGroupId(), s.getPrivateGroupId(), localTimestamp, @@ -191,6 +199,10 @@ abstract class AbstractProtocolEngine> s.getLastLocalMessageId(), timer); sendMessage(txn, m, LEAVE, s.getPrivateGroupId(), visibleInUi, timer); + // Set the auto-delete timer duration on the message + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } } else { m = messageEncoder.encodeLeaveMessage(s.getContactGroupId(), s.getPrivateGroupId(), localTimestamp, diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java index c34a23c3e..f2036b4f5 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java @@ -1,6 +1,7 @@ package org.briarproject.briar.privategroup.invitation; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.cleanup.CleanupHook; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ContactGroupFactory; import org.briarproject.bramble.api.contact.Contact; @@ -24,6 +25,7 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; +import org.briarproject.briar.api.autodelete.event.ConversationMessagesDeletedEvent; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationMessageHeader; @@ -52,6 +54,7 @@ import javax.annotation.concurrent.Immutable; import javax.inject.Inject; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.CreatorState.START; import static org.briarproject.briar.privategroup.invitation.MessageType.ABORT; import static org.briarproject.briar.privategroup.invitation.MessageType.INVITE; @@ -65,7 +68,7 @@ import static org.briarproject.briar.privategroup.invitation.Role.PEER; @NotNullByDefault class GroupInvitationManagerImpl extends ConversationClientImpl implements GroupInvitationManager, OpenDatabaseHook, ContactHook, - PrivateGroupHook, ClientVersioningHook { + PrivateGroupHook, ClientVersioningHook, CleanupHook { private final ClientVersioningManager clientVersioningManager; private final ContactGroupFactory contactGroupFactory; @@ -148,6 +151,11 @@ class GroupInvitationManagerImpl extends ConversationClientImpl BdfDictionary bdfMeta) throws DbException, FormatException { // Parse the metadata MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // set the clean-up timer that will be started when message gets read + long timer = meta.getAutoDeleteTimer(); + if (timer != NO_AUTO_DELETE_TIMER) { + db.setCleanupTimerDuration(txn, m.getId(), timer); + } // Look up the session, if there is one SessionId sessionId = getSessionId(meta.getPrivateGroupId()); StoredSession ss = getSession(txn, m.getGroupId(), sessionId); @@ -301,7 +309,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl @Override public void respondToInvitation(ContactId c, SessionId sessionId, boolean accept) throws DbException { - Transaction txn = db.startTransaction(false); + db.transaction(false, + txn -> respondToInvitation(txn, c, sessionId, accept)); + } + + private void respondToInvitation(Transaction txn, ContactId c, + SessionId sessionId, boolean accept) throws DbException { try { // Look up the session Contact contact = db.getContact(txn, c); @@ -316,11 +329,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl else session = inviteeEngine.onLeaveAction(txn, session); // Store the updated session storeSession(txn, ss.storageId, session); - db.commitTransaction(txn); } catch (FormatException e) { throw new DbException(e); - } finally { - db.endTransaction(txn); } } @@ -686,7 +696,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl // get ID of the contact group GroupId g = getContactGroup(db.getContact(txn, c)).getId(); - // get metadata for all messages in the + // get metadata for all messages in the group // (these are sessions *and* protocol messages) Map metadata; try { @@ -762,6 +772,59 @@ class GroupInvitationManagerImpl extends ConversationClientImpl return result; } + @Override + public void deleteMessages(Transaction txn, GroupId g, + Collection messageIds) throws DbException { + ContactId c; + Map sessions = new HashMap<>(); + try { + // get the ContactId from the given GroupId + c = clientHelper.getContactId(txn, g); + // get sessions for all messages to be deleted + for (MessageId messageId : messageIds) { + BdfDictionary d = clientHelper + .getMessageMetadataAsDictionary(txn, messageId); + MessageMetadata messageMetadata = + messageParser.parseMetadata(d); + if (!messageMetadata.isVisibleInConversation()) + throw new IllegalArgumentException(); + SessionId sessionId = + getSessionId(messageMetadata.getPrivateGroupId()); + DeletableSession deletableSession = sessions.get(sessionId); + if (deletableSession == null) { + StoredSession ss = getSession(txn, g, sessionId); + if (ss == null) throw new DbException(); + BdfDictionary sessionMeta = clientHelper + .getMessageMetadataAsDictionary(txn, ss.storageId); + Session session = sessionParser + .parseSession(g, sessionMeta); + deletableSession = new DeletableSession(session.getState()); + sessions.put(sessionId, deletableSession); + } + deletableSession.messages.add(messageId); + } + } catch (FormatException e) { + throw new DbException(e); + } + + // delete given visible messages in sessions and auto-respond before + for (Entry entry : sessions.entrySet()) { + DeletableSession session = entry.getValue(); + // decline invitee sessions waiting for a response before + if (session.state instanceof InviteeState && + session.state.isAwaitingResponse()) { + respondToInvitation(txn, c, entry.getKey(), false); + } + for (MessageId m : session.messages) { + db.deleteMessage(txn, m); + db.deleteMessageMetadata(txn, m); + } + } + recalculateGroupCount(txn, g); + + txn.attach(new ConversationMessagesDeletedEvent(c, messageIds)); + } + @Override public Set getMessageIds(Transaction txn, ContactId c) throws DbException { diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java index 0628357e3..fa0b09019 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationModule.java @@ -1,5 +1,6 @@ package org.briarproject.briar.privategroup.invitation; +import org.briarproject.bramble.api.cleanup.CleanupManager; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; @@ -41,7 +42,8 @@ public class GroupInvitationModule { ValidationManager validationManager, ContactManager contactManager, PrivateGroupManager privateGroupManager, ConversationManager conversationManager, - ClientVersioningManager clientVersioningManager) { + ClientVersioningManager clientVersioningManager, + CleanupManager cleanupManager) { lifecycleManager.registerOpenDatabaseHook(groupInvitationManager); validationManager.registerIncomingMessageHook(CLIENT_ID, MAJOR_VERSION, groupInvitationManager); @@ -56,6 +58,8 @@ public class GroupInvitationModule { PrivateGroupManager.MAJOR_VERSION, PrivateGroupManager.MINOR_VERSION, groupInvitationManager.getPrivateGroupClientVersioningHook()); + cleanupManager.registerCleanupHook(CLIENT_ID, MAJOR_VERSION, + groupInvitationManager); return groupInvitationManager; } diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java index 3785a649e..01a3e79a3 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java @@ -21,6 +21,7 @@ import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.DbExpectations; import org.briarproject.bramble.test.TestUtils; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; @@ -329,10 +330,11 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { context.checking(new Expectations() {{ oneOf(messageParser).parseMetadata(meta); will(returnValue(messageMetadata)); + oneOf(messageMetadata).getAutoDeleteTimer(); + will(returnValue(NO_AUTO_DELETE_TIMER)); oneOf(messageMetadata).getPrivateGroupId(); will(returnValue(privateGroup.getId())); }}); - } private void expectIncomingMessage(Role role, MessageType type) @@ -530,15 +532,13 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { public void testRespondToInvitationWithoutSession() throws Exception { SessionId sessionId = new SessionId(getRandomId()); - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).getContact(txn, contactId); will(returnValue(contact)); oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, MAJOR_VERSION, contact); will(returnValue(contactGroup)); - oneOf(db).endTransaction(txn); }}); expectGetSession(noResults, sessionId, contactGroup.getId()); @@ -582,9 +582,8 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { private void expectRespondToInvitation(SessionId sessionId, boolean accept) throws Exception { expectGetSession(oneResult, sessionId, contactGroup.getId()); - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); + context.checking(new DbExpectations() {{ + oneOf(db).transaction(with(false), withDbRunnable(txn)); oneOf(db).getContact(txn, contactId); will(returnValue(contact)); oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, @@ -596,8 +595,6 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { if (accept) oneOf(inviteeEngine).onJoinAction(txn, inviteeSession); else oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession); will(returnValue(inviteeSession)); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); }}); expectStoreSession(inviteeSession, storageMessage.getId()); } From ebd233d00540e695591aba0480991d64ec9d19c6 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Mar 2021 13:59:16 -0300 Subject: [PATCH 2/6] Factor out auto-delete integration test code so we can re-use it in other tests --- .../autodelete/AbstractAutoDeleteTest.java | 180 ++++++++++++++++++ .../messaging/AutoDeleteIntegrationTest.java | 149 +-------------- .../briar/test/BriarTestUtils.java | 2 +- 3 files changed, 187 insertions(+), 144 deletions(-) create mode 100644 briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java diff --git a/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java new file mode 100644 index 000000000..74acfd83b --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java @@ -0,0 +1,180 @@ +package org.briarproject.briar.autodelete; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.event.Event; +import org.briarproject.bramble.api.event.EventBus; +import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.system.TimeTravelModule; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.briar.api.autodelete.AutoDeleteManager; +import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; +import org.briarproject.briar.api.conversation.ConversationMessageHeader; +import org.briarproject.briar.api.messaging.MessagingManager; +import org.briarproject.briar.test.BriarIntegrationTest; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; +import org.junit.Before; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import javax.annotation.Nonnull; + +import static java.util.Collections.sort; +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +public abstract class AbstractAutoDeleteTest extends + BriarIntegrationTest { + + protected final long startTime = System.currentTimeMillis(); + + protected abstract ConversationClient getConversationClient( + BriarIntegrationTestComponent component); + + @Override + protected void createComponents() { + BriarIntegrationTestComponent component = + DaggerBriarIntegrationTestComponent.builder().build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(component); + component.inject(this); + + c0 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0); + + c1 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1); + + c2 = DaggerBriarIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir)) + .timeTravelModule(new TimeTravelModule(true)) + .build(); + BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2); + + // Use different times to avoid creating identical messages that are + // treated as redundant copies of the same message (#1907) + try { + c0.getTimeTravel().setCurrentTimeMillis(startTime); + c1.getTimeTravel().setCurrentTimeMillis(startTime + 1); + c2.getTimeTravel().setCurrentTimeMillis(startTime + 2); + } catch (InterruptedException e) { + fail(); + } + } + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + // Run the initial cleanup task that was scheduled at startup + c0.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + c1.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + c2.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + } + + protected List getMessageHeaders( + BriarIntegrationTestComponent component, ContactId contactId) + throws Exception { + DatabaseComponent db = component.getDatabaseComponent(); + ConversationClient conversationClient = + getConversationClient(component); + return sortHeaders(db.transactionWithResult(true, txn -> + conversationClient.getMessageHeaders(txn, contactId))); + } + + @SuppressWarnings({"UseCompareMethod", "Java8ListSort"}) // Animal Sniffer + protected List sortHeaders( + Collection in) { + List out = new ArrayList<>(in); + sort(out, (a, b) -> + Long.valueOf(a.getTimestamp()).compareTo(b.getTimestamp())); + return out; + } + + @FunctionalInterface + protected interface HeaderConsumer { + void accept(ConversationMessageHeader header); + } + + protected void forEachHeader(BriarIntegrationTestComponent component, + ContactId contactId, int size, HeaderConsumer consumer) + throws Exception { + List headers = + getMessageHeaders(component, contactId); + assertEquals(size, headers.size()); + for (ConversationMessageHeader h : headers) consumer.accept(h); + } + + protected long getAutoDeleteTimer(BriarIntegrationTestComponent component, + ContactId contactId) throws DbException { + DatabaseComponent db = component.getDatabaseComponent(); + AutoDeleteManager autoDeleteManager = component.getAutoDeleteManager(); + return db.transactionWithResult(false, + txn -> autoDeleteManager.getAutoDeleteTimer(txn, contactId)); + } + + protected void markMessageRead(BriarIntegrationTestComponent component, + Contact contact, MessageId messageId) throws Exception { + MessagingManager messagingManager = component.getMessagingManager(); + ConversationClient conversationClient = + getConversationClient(component); + GroupId groupId = conversationClient.getContactGroup(contact).getId(); + messagingManager.setReadFlag(groupId, messageId, true); + waitForEvents(component); + } + + protected void assertGroupCount(BriarIntegrationTestComponent component, + ContactId contactId, int messageCount, int unreadCount) + throws DbException { + DatabaseComponent db = component.getDatabaseComponent(); + ConversationClient conversationClient = + getConversationClient(component); + + GroupCount gc = db.transactionWithResult(true, txn -> + conversationClient.getGroupCount(txn, contactId)); + assertEquals("messageCount", messageCount, gc.getMsgCount()); + assertEquals("unreadCount", unreadCount, gc.getUnreadCount()); + } + + /** + * Broadcasts a marker event and waits for it to be delivered, which + * indicates that all previously broadcast events have been delivered. + */ + protected void waitForEvents(BriarIntegrationTestComponent component) + throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MarkerEvent marker = new MarkerEvent(); + EventBus eventBus = component.getEventBus(); + eventBus.addListener(new EventListener() { + @Override + public void eventOccurred(@Nonnull Event e) { + if (e == marker) { + latch.countDown(); + eventBus.removeListener(this); + } + } + }); + eventBus.broadcast(marker); + if (!latch.await(1, MINUTES)) fail(); + } + + private static class MarkerEvent extends Event { + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java index da5363e12..911eedcf8 100644 --- a/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/messaging/AutoDeleteIntegrationTest.java @@ -1,44 +1,29 @@ package org.briarproject.briar.messaging; -import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.MessageDeletedException; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.event.EventBus; -import org.briarproject.bramble.api.event.EventListener; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.system.TimeTravelModule; -import org.briarproject.bramble.test.TestDatabaseConfigModule; import org.briarproject.briar.api.attachment.AttachmentHeader; import org.briarproject.briar.api.autodelete.AutoDeleteManager; -import org.briarproject.briar.api.client.MessageTracker.GroupCount; import org.briarproject.briar.api.conversation.ConversationManager; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; import org.briarproject.briar.api.conversation.ConversationMessageHeader; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessage; import org.briarproject.briar.api.messaging.PrivateMessageFactory; -import org.briarproject.briar.test.BriarIntegrationTest; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; import org.briarproject.briar.test.BriarIntegrationTestComponent; -import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent; -import org.junit.Before; import org.junit.Test; import java.io.ByteArrayInputStream; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; -import java.util.concurrent.CountDownLatch; - -import javax.annotation.Nonnull; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; -import static java.util.Collections.sort; -import static java.util.concurrent.TimeUnit.MINUTES; import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; @@ -47,56 +32,13 @@ import static org.briarproject.briar.messaging.MessagingConstants.MISSING_ATTACH import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -public class AutoDeleteIntegrationTest - extends BriarIntegrationTest { +public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { @Override - protected void createComponents() { - BriarIntegrationTestComponent component = - DaggerBriarIntegrationTestComponent.builder().build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(component); - component.inject(this); - - c0 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t0Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0); - - c1 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t1Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1); - - c2 = DaggerBriarIntegrationTestComponent.builder() - .testDatabaseConfigModule(new TestDatabaseConfigModule(t2Dir)) - .timeTravelModule(new TimeTravelModule(true)) - .build(); - BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2); - - // Use different times to avoid creating identical messages that are - // treated as redundant copies of the same message (#1907) - try { - long now = System.currentTimeMillis(); - c0.getTimeTravel().setCurrentTimeMillis(now); - c1.getTimeTravel().setCurrentTimeMillis(now + 1); - c2.getTimeTravel().setCurrentTimeMillis(now + 2); - } catch (InterruptedException e) { - fail(); - } - } - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - // Run the initial cleanup task that was scheduled at startup - c0.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); - c1.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); - c2.getTimeTravel().addCurrentTimeMillis(BATCH_DELAY_MS); + protected ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getMessagingManager(); } @Test @@ -216,7 +158,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -293,7 +234,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId0); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -355,7 +295,6 @@ public class AutoDeleteIntegrationTest assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); // 0 marks the message as read - this starts 0's timer markMessageRead(c0, contact1From0, messageId1); - waitForEvents(c0); assertGroupCount(c0, contactId1From0, 1, 0); // Before 0's timer elapses, 0 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -426,7 +365,6 @@ public class AutoDeleteIntegrationTest assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -501,7 +439,6 @@ public class AutoDeleteIntegrationTest assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -603,7 +540,6 @@ public class AutoDeleteIntegrationTest assertTrue(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -711,7 +647,6 @@ public class AutoDeleteIntegrationTest assertFalse(messageIsDeleted(c1, attachmentHeader.getMessageId())); // 1 marks the message as read - this starts 1's timer markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); @@ -808,33 +743,6 @@ public class AutoDeleteIntegrationTest db.transaction(false, txn -> db.setMessageShared(txn, messageId)); } - private List getMessageHeaders( - BriarIntegrationTestComponent component, ContactId contactId) - throws Exception { - DatabaseComponent db = component.getDatabaseComponent(); - MessagingManager messagingManager = component.getMessagingManager(); - - return sortHeaders(db.transactionWithResult(true, txn -> - messagingManager.getMessageHeaders(txn, contactId))); - } - - private long getAutoDeleteTimer(BriarIntegrationTestComponent component, - ContactId contactId) throws DbException { - DatabaseComponent db = component.getDatabaseComponent(); - AutoDeleteManager autoDeleteManager = component.getAutoDeleteManager(); - - return db.transactionWithResult(false, - txn -> autoDeleteManager.getAutoDeleteTimer(txn, contactId)); - } - - private void markMessageRead(BriarIntegrationTestComponent component, - Contact contact, MessageId messageId) throws DbException { - MessagingManager messagingManager = component.getMessagingManager(); - - GroupId groupId = messagingManager.getContactGroup(contact).getId(); - messagingManager.setReadFlag(groupId, messageId, true); - } - private boolean messageIsDeleted(BriarIntegrationTestComponent component, MessageId messageId) throws DbException { DatabaseComponent db = component.getDatabaseComponent(); @@ -847,49 +755,4 @@ public class AutoDeleteIntegrationTest } } - private void assertGroupCount(BriarIntegrationTestComponent component, - ContactId contactId, int messageCount, int unreadCount) - throws DbException { - DatabaseComponent db = component.getDatabaseComponent(); - MessagingManager messagingManager = component.getMessagingManager(); - - GroupCount gc = db.transactionWithResult(true, txn -> - messagingManager.getGroupCount(txn, contactId)); - assertEquals(messageCount, gc.getMsgCount()); - assertEquals(unreadCount, gc.getUnreadCount()); - } - - @SuppressWarnings({"UseCompareMethod", "Java8ListSort"}) // Animal Sniffer - private List sortHeaders( - Collection in) { - List out = new ArrayList<>(in); - sort(out, (a, b) -> - Long.valueOf(a.getTimestamp()).compareTo(b.getTimestamp())); - return out; - } - - /** - * Broadcasts a marker event and waits for it to be delivered, which - * indicates that all previously broadcast events have been delivered. - */ - private void waitForEvents(BriarIntegrationTestComponent component) - throws Exception { - CountDownLatch latch = new CountDownLatch(1); - MarkerEvent marker = new MarkerEvent(); - EventBus eventBus = component.getEventBus(); - eventBus.addListener(new EventListener() { - @Override - public void eventOccurred(@Nonnull Event e) { - if (e == marker) { - latch.countDown(); - eventBus.removeListener(this); - } - } - }); - eventBus.broadcast(marker); - if (!latch.await(1, MINUTES)) fail(); - } - - private static class MarkerEvent extends Event { - } } diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java index 8f6f97226..6d50b08ac 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java @@ -51,7 +51,7 @@ public class BriarTestUtils { byte[] linkBytes = new byte[RAW_LINK_BYTES]; byte[] publicKey = keyPair.getPublic().getEncoded(); linkBytes[0] = FORMAT_VERSION; - arraycopy(publicKey,0, linkBytes, 1, RAW_LINK_BYTES - 1); + arraycopy(publicKey, 0, linkBytes, 1, RAW_LINK_BYTES - 1); return ("briar://" + Base32.encode(linkBytes)).toLowerCase(); } From 63012d0a7262a7e430eb2e3c1860e195cf84bdfb Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Mar 2021 13:59:45 -0300 Subject: [PATCH 3/6] Add integration tests for auto-deletion of private group invitations and responses --- .../invitation/AutoDeleteIntegrationTest.java | 411 ++++++++++++++++++ .../test/BriarIntegrationTestComponent.java | 3 + 2 files changed, 414 insertions(+) create mode 100644 briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java new file mode 100644 index 000000000..633a55f89 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java @@ -0,0 +1,411 @@ +package org.briarproject.briar.privategroup.invitation; + +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; +import org.briarproject.briar.api.conversation.ConversationMessageHeader; +import org.briarproject.briar.api.privategroup.GroupMessage; +import org.briarproject.briar.api.privategroup.PrivateGroup; +import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationRequest; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; +import org.briarproject.briar.autodelete.AbstractAutoDeleteTest; +import org.briarproject.briar.test.BriarIntegrationTestComponent; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +import javax.annotation.Nullable; + +import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.MIN_AUTO_DELETE_TIMER_MS; +import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { + + private PrivateGroup privateGroup; + private PrivateGroupManager groupManager0; + private GroupInvitationManager groupInvitationManager0, + groupInvitationManager1; + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + groupManager0 = c0.getPrivateGroupManager(); + groupInvitationManager0 = c0.getGroupInvitationManager(); + groupInvitationManager1 = c1.getGroupInvitationManager(); + privateGroup = addPrivateGroup("Testgroup", startTime); + } + + @Override + protected ConversationClient getConversationClient( + BriarIntegrationTestComponent component) { + return component.getGroupInvitationManager(); + } + + @Test + public void testInvitationAutoDecline() throws Exception { + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + + // The message should have been added to 0's view of the conversation + assertGroupCount(c0, contactId1From0, 1, 0); + forEachHeader(c0, contactId1From0, 1, h -> { + // The message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer()); + }); + + // Sync the message to 1 + sync0To1(1, true); + // Sync the ack to 0 - this starts 0's timer + ack1To0(1); + waitForEvents(c0); + + // The message should have been added to 1's view of the conversation + assertGroupCount(c1, contactId0From1, 1, 1); + forEachHeader(c1, contactId0From1, 1, h -> { + // The message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer()); + }); + + // Both peers should be using the new timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c0, contactId1From0)); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c1, contactId0From1)); + + // Before 0's timer elapses, both peers should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 marks the message as read - this starts 1's timer + final MessageId messageId = + getMessageHeaders(c1, contactId0From1).get(0).getId(); + markMessageRead(c1, contact0From1, messageId); + waitForEvents(c1); + assertGroupCount(c1, contactId0From1, 1, 0); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation and the invitation auto-declined + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + forEachHeader(c1, contactId0From1, 1, h -> { + // The only message is not the same as before, but declined response + assertNotEquals(messageId, h.getId()); + assertTrue(h instanceof GroupInvitationResponse); + assertFalse(((GroupInvitationResponse) h).wasAccepted()); + // The auto-decline message should have the expected timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + h.getAutoDeleteTimer()); + }); + + // Sync the auto-decline message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + + // 0 can invite 1 again + assertTrue(groupInvitationManager0 + .isInvitationAllowed(contact1From0, privateGroup.getId())); + + // Before 1's timer elapses, 1 should still see the auto-decline message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + // When 1's timer has elapsed, the auto-decline message should be + // deleted from 1's view of the conversation + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 0, 0); + assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); + + // 0 marks the message as read - this starts 0's timer + MessageId messageId0 = + getMessageHeaders(c0, contactId1From0).get(0).getId(); + markMessageRead(c0, contact1From0, messageId0); + waitForEvents(c0); + assertGroupCount(c0, contactId1From0, 1, 0); + + // Before 0's timer elapses, 0 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + + // 0 can invite 1 again and really does invite + assertTrue(groupInvitationManager0 + .isInvitationAllowed(contact1From0, privateGroup.getId())); + sendInvitation(privateGroup, contact1From0.getId(), + "Join this faster please!"); + sync0To1(1, true); + assertGroupCount(c1, contactId0From1, 1, 1); + } + + @Test + public void testAutoDeleteDoesNotRemoveOtherSessions() throws Exception { + PrivateGroup pg = addPrivateGroup("Another one", startTime + 1); + + // Send invitation for another group without timer + sendInvitation(pg, contact1From0.getId(), null); + sync0To1(1, true); + ack1To0(1); + + // The message should have been added the views of the conversation + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 1, 1); + // The message should have no timer for either peer + forEachHeader(c0, contactId1From0, 1, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + forEachHeader(c1, contactId0From1, 1, h -> + assertEquals(NO_AUTO_DELETE_TIMER, h.getAutoDeleteTimer())); + + // enable timer + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation, ACK it and check group counts + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + sync0To1(1, true); + ack1To0(1); + waitForEvents(c0); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 2, 2); + + // Both peers should be using the new timer + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c0, contactId1From0)); + assertEquals(MIN_AUTO_DELETE_TIMER_MS, + getAutoDeleteTimer(c1, contactId0From1)); + + // Before 0's timer elapses, both peers should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 0); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 2); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 2); + List headers1 = + getMessageHeaders(c1, contactId0From1); + assertEquals(2, headers1.size()); + + // 1 marks the message as read - this starts 1's timer + ConversationMessageHeader header = headers1.get(1); // newer message + assertEquals(privateGroup, + ((GroupInvitationRequest) header).getNameable()); + MessageId messageId = header.getId(); + markMessageRead(c1, contact0From1, messageId); + waitForEvents(c1); + assertGroupCount(c1, contactId0From1, 2, 1); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 1); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation and the invitation auto-declined + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 1); + forEachHeader(c1, contactId0From1, 2, h -> { + if (h instanceof GroupInvitationRequest) { + // the request is for the first invitation + assertEquals(pg.getId(), + ((GroupInvitationRequest) h).getNameable().getId()); + } else { + assertTrue(h instanceof GroupInvitationResponse); + GroupInvitationResponse r = (GroupInvitationResponse) h; + // is auto-decline for 2nd invitation + assertEquals(privateGroup.getId(), r.getShareableId()); + assertFalse(r.wasAccepted()); + } + }); + + // Sync the auto-decline message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + // 0 marks the message as read - this starts 0's timer + GroupInvitationResponse autoDeclineMessage = (GroupInvitationResponse) + getMessageHeaders(c1, contactId0From1).get(1); + markMessageRead(c0, contact1From0, autoDeclineMessage.getId()); + waitForEvents(c0); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 2, 1); + + // Timer of auto-decline elapses for both peers at the same time + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 1, 1); + + // 1 responds to first invitation (that had no timer) + groupInvitationManager1.respondToInvitation(contactId0From1, pg, true); + // Sync the accept response message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 1); + + // Before 1's timer elapses, 1 should still see the message + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 2, 1); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); + + // When 1's timer has elapsed, the message should be deleted from 1's + // view of the conversation + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 2, 1); + assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + forEachHeader(c1, contactId0From1, 1, h -> { + assertTrue(h instanceof GroupInvitationRequest); + assertTrue(((GroupInvitationRequest) h).wasAnswered()); + assertTrue(((GroupInvitationRequest) h).canBeOpened()); + }); + } + + @Test + public void testResponseAfterSenderDeletedInvitation() throws Exception { + setAutoDeleteTimer(c0, contact1From0.getId(), MIN_AUTO_DELETE_TIMER_MS); + + // Send invitation + sendInvitation(privateGroup, contact1From0.getId(), "Join this!"); + assertGroupCount(c0, contactId1From0, 1, 0); + + // Sync the message to 1 + sync0To1(1, true); + // Sync the ack to 0 - this starts 0's timer + ack1To0(1); + waitForEvents(c0); + assertGroupCount(c1, contactId0From1, 1, 1); + + // When 0's timer has elapsed, the message should be deleted from 0's + // view of the conversation but 1 should still see the message + long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS; + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 1); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 marks message as read - this starts 1's timer + markMessageRead(c1, contact0From1, + getMessageHeaders(c1, contactId0From1).get(0).getId()); + + // 1 responds to invitation + groupInvitationManager1 + .respondToInvitation(contactId0From1, privateGroup, true); + // Sync the accept response message to 0 + sync1To0(1, true); + // Sync the ack to 1 - this starts 1's timer + ack0To1(1); + waitForEvents(c1); + assertGroupCount(c0, contactId1From0, 1, 1); + assertGroupCount(c1, contactId0From1, 2, 0); + + // 0 marks the message as read - this starts 0's timer + GroupInvitationResponse autoDeclineMessage = (GroupInvitationResponse) + getMessageHeaders(c1, contactId0From1).get(1); + markMessageRead(c0, contact1From0, autoDeclineMessage.getId()); + waitForEvents(c0); + assertGroupCount(c0, contactId1From0, 1, 0); + assertGroupCount(c1, contactId0From1, 2, 0); + + // 1 joined the PrivateGroup + assertEquals(privateGroup, c1.getPrivateGroupManager() + .getPrivateGroup(privateGroup.getId())); + assertFalse(groupInvitationManager0 + .isInvitationAllowed(contact1From0, privateGroup.getId())); + } + + private PrivateGroup addPrivateGroup(String name, long timestamp) + throws DbException { + PrivateGroup pg = privateGroupFactory.createPrivateGroup(name, author0); + GroupMessage joinMsg0 = groupMessageFactory + .createJoinMessage(pg.getId(), timestamp, author0); + groupManager0.addPrivateGroup(pg, joinMsg0, true); + return pg; + } + + private void sendInvitation(PrivateGroup pg, ContactId contactId, + @Nullable String text) throws DbException { + DatabaseComponent db0 = c0.getDatabaseComponent(); + long timestamp = db0.transactionWithResult(true, txn -> + c0.getConversationManager() + .getTimestampForOutgoingMessage(txn, contactId)); + byte[] signature = groupInvitationFactory.signInvitation(contact1From0, + pg.getId(), timestamp, author0.getPrivateKey()); + long timer = getAutoDeleteTimer(c0, contactId, timestamp); + groupInvitationManager0.sendInvitation(pg.getId(), contactId, text, + timestamp, signature, timer); + } +} diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java index d0a4ce577..738b48e96 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java @@ -28,6 +28,7 @@ import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.messaging.MessagingManager; import org.briarproject.briar.api.messaging.PrivateMessageFactory; import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationFactory; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; import org.briarproject.briar.attachment.AttachmentModule; import org.briarproject.briar.autodelete.AutoDeleteModule; @@ -120,6 +121,8 @@ public interface BriarIntegrationTestComponent GroupInvitationManager getGroupInvitationManager(); + GroupInvitationFactory getGroupInvitationFactory(); + IntroductionManager getIntroductionManager(); MessageTracker getMessageTracker(); From 012ab0310febeda6a55fd3bf856a4822e8fe0c0d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 1 Mar 2021 17:00:16 -0300 Subject: [PATCH 4/6] Remember when declines were automatic due to deletion so they can be shown differently for sender --- .../api/blog/BlogInvitationResponse.java | 2 +- .../conversation/ConversationResponse.java | 9 +- .../api/forum/ForumInvitationResponse.java | 2 +- .../introduction/IntroductionResponse.java | 4 +- .../invitation/GroupInvitationResponse.java | 4 +- .../briar/api/sharing/InvitationResponse.java | 4 +- .../invitation/AbstractProtocolEngine.java | 25 +++- .../invitation/CreatorProtocolEngine.java | 8 +- .../invitation/GroupInvitationConstants.java | 1 + .../GroupInvitationManagerImpl.java | 15 +- .../invitation/GroupInvitationValidator.java | 13 +- .../invitation/InviteeProtocolEngine.java | 14 +- .../invitation/MessageEncoder.java | 6 +- .../invitation/MessageEncoderImpl.java | 13 +- .../invitation/MessageMetadata.java | 11 +- .../invitation/MessageParserImpl.java | 4 +- .../invitation/PeerProtocolEngine.java | 8 +- .../invitation/ProtocolEngine.java | 9 +- .../AbstractProtocolEngineTest.java | 2 +- .../invitation/AutoDeleteIntegrationTest.java | 133 ++++++++++++------ .../invitation/CreatorProtocolEngineTest.java | 12 +- .../GroupInvitationManagerImplTest.java | 12 +- .../GroupInvitationValidatorTest.java | 3 +- .../invitation/InviteeProtocolEngineTest.java | 14 +- .../invitation/PeerProtocolEngineTest.java | 14 +- 25 files changed, 218 insertions(+), 124 deletions(-) diff --git a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java index 37cfef49a..8d0c9cbef 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/blog/BlogInvitationResponse.java @@ -15,7 +15,7 @@ public class BlogInvitationResponse extends InvitationResponse { SessionId sessionId, boolean accept, GroupId shareableId, long autoDeleteTimer) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, false); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java index 974d87c4e..19abb9ea7 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/conversation/ConversationResponse.java @@ -12,14 +12,16 @@ import javax.annotation.concurrent.Immutable; public abstract class ConversationResponse extends ConversationMessageHeader { private final SessionId sessionId; - private final boolean accepted; + private final boolean accepted, isAutoDecline; public ConversationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, - SessionId sessionId, boolean accepted, long autoDeleteTimer) { + SessionId sessionId, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, autoDeleteTimer); this.sessionId = sessionId; this.accepted = accepted; + this.isAutoDecline = isAutoDecline; } public SessionId getSessionId() { @@ -30,4 +32,7 @@ public abstract class ConversationResponse extends ConversationMessageHeader { return accepted; } + public boolean isAutoDecline() { + return isAutoDecline; + } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java index be3757e35..033d7ad37 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/forum/ForumInvitationResponse.java @@ -18,7 +18,7 @@ public class ForumInvitationResponse extends InvitationResponse { SessionId sessionId, boolean accept, GroupId shareableId, long autoDeleteTimer) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, false); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java index 705b10a24..d05baadc0 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java @@ -1,13 +1,13 @@ package org.briarproject.briar.api.introduction; import org.briarproject.bramble.api.identity.Author; -import org.briarproject.briar.api.identity.AuthorInfo; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationMessageVisitor; import org.briarproject.briar.api.conversation.ConversationResponse; +import org.briarproject.briar.api.identity.AuthorInfo; import javax.annotation.concurrent.Immutable; @@ -28,7 +28,7 @@ public class IntroductionResponse extends ConversationResponse { AuthorInfo introducedAuthorInfo, Role role, boolean canSucceed, long autoDeleteTimer) { super(messageId, groupId, time, local, read, sent, seen, sessionId, - accepted, autoDeleteTimer); + accepted, autoDeleteTimer, false); this.introducedAuthor = author; this.introducedAuthorInfo = introducedAuthorInfo; this.ourRole = role; diff --git a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java index b461d9dc1..0f7d06367 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/privategroup/invitation/GroupInvitationResponse.java @@ -16,9 +16,9 @@ public class GroupInvitationResponse extends InvitationResponse { public GroupInvitationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, SessionId sessionId, boolean accept, GroupId shareableId, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, sessionId, - accept, shareableId, autoDeleteTimer); + accept, shareableId, autoDeleteTimer, isAutoDecline); } @Override diff --git a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java index fd14fa051..18d08fd78 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/sharing/InvitationResponse.java @@ -12,9 +12,9 @@ public abstract class InvitationResponse extends ConversationResponse { public InvitationResponse(MessageId id, GroupId groupId, long time, boolean local, boolean read, boolean sent, boolean seen, SessionId sessionId, boolean accepted, GroupId shareableId, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { super(id, groupId, time, local, read, sent, seen, sessionId, accepted, - autoDeleteTimer); + autoDeleteTimer, isAutoDecline); this.shareableId = shareableId; } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java index 2bf722db0..9efabfa1f 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java @@ -180,15 +180,20 @@ abstract class AbstractProtocolEngine> return m; } - Message sendLeaveMessage(Transaction txn, S s, boolean visibleInUi) - throws DbException { + Message sendLeaveMessage(Transaction txn, S s) throws DbException { + return sendLeaveMessage(txn, s, false, false); + } + + Message sendLeaveMessage(Transaction txn, S s, boolean visibleInUi, + boolean isAutoDecline) throws DbException { + if (!visibleInUi && isAutoDecline) throw new IllegalArgumentException(); Message m; long localTimestamp = visibleInUi ? getTimestampForVisibleMessage(txn, s) : getTimestampForInvisibleMessage(s); ContactId c = getContactId(txn, s.getContactGroupId()); if (contactSupportsAutoDeletion(txn, c)) { - // Set auto-delete timer if manually accepting an invitation + // Set auto-delete timer if declining an invitation long timer = NO_AUTO_DELETE_TIMER; if (visibleInUi) { timer = autoDeleteManager.getAutoDeleteTimer(txn, c, @@ -198,8 +203,8 @@ abstract class AbstractProtocolEngine> s.getPrivateGroupId(), localTimestamp, s.getLastLocalMessageId(), timer); sendMessage(txn, m, LEAVE, s.getPrivateGroupId(), visibleInUi, - timer); - // Set the auto-delete timer duration on the message + timer, isAutoDecline); + // Set the auto-delete timer duration on the local message if (timer != NO_AUTO_DELETE_TIMER) { db.setCleanupTimerDuration(txn, m.getId(), timer); } @@ -321,9 +326,17 @@ abstract class AbstractProtocolEngine> private void sendMessage(Transaction txn, Message m, MessageType type, GroupId privateGroupId, boolean visibleInConversation, long autoDeleteTimer) throws DbException { + sendMessage(txn, m, type, privateGroupId, visibleInConversation, + autoDeleteTimer, false); + } + + private void sendMessage(Transaction txn, Message m, MessageType type, + GroupId privateGroupId, boolean visibleInConversation, + long autoDeleteTimer, boolean isAutoDecline) throws DbException { BdfDictionary meta = messageEncoder.encodeMetadata(type, privateGroupId, m.getTimestamp(), true, true, - visibleInConversation, false, false, autoDeleteTimer); + visibleInConversation, false, false, autoDeleteTimer, + isAutoDecline); try { clientHelper.addLocalMessage(txn, m, meta, true, false); } catch (FormatException e) { diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java index 07ff7a802..e8d85fc34 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngine.java @@ -84,8 +84,8 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { } @Override - public CreatorSession onLeaveAction(Transaction txn, CreatorSession s) - throws DbException { + public CreatorSession onLeaveAction(Transaction txn, CreatorSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case DISSOLVED: @@ -180,7 +180,7 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { throw new DbException(e); // Invalid group metadata } // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); // Move to the DISSOLVED state return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), @@ -276,6 +276,6 @@ class CreatorProtocolEngine extends AbstractProtocolEngine { SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); return new GroupInvitationResponse(m.getId(), m.getContactGroupId(), m.getTimestamp(), false, false, false, false, sessionId, - accept, m.getPrivateGroupId(), m.getAutoDeleteTimer()); + accept, m.getPrivateGroupId(), m.getAutoDeleteTimer(), false); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java index 82efba05f..ede12734a 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationConstants.java @@ -11,6 +11,7 @@ interface GroupInvitationConstants { String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer"; String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted"; String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer"; + String MSG_KEY_IS_AUTO_DECLINE = "isAutoDecline"; // Session keys String SESSION_KEY_IS_SESSION = "isSession"; diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java index f2036b4f5..acecd6038 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java @@ -310,11 +310,12 @@ class GroupInvitationManagerImpl extends ConversationClientImpl public void respondToInvitation(ContactId c, SessionId sessionId, boolean accept) throws DbException { db.transaction(false, - txn -> respondToInvitation(txn, c, sessionId, accept)); + txn -> respondToInvitation(txn, c, sessionId, accept, false)); } private void respondToInvitation(Transaction txn, ContactId c, - SessionId sessionId, boolean accept) throws DbException { + SessionId sessionId, boolean accept, boolean isAutoDecline) + throws DbException { try { // Look up the session Contact contact = db.getContact(txn, c); @@ -326,7 +327,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl .parseInviteeSession(contactGroupId, ss.bdfSession); // Handle the join or leave action if (accept) session = inviteeEngine.onJoinAction(txn, session); - else session = inviteeEngine.onLeaveAction(txn, session); + else session = + inviteeEngine.onLeaveAction(txn, session, isAutoDecline); // Store the updated session storeSession(txn, ss.storageId, session); } catch (FormatException e) { @@ -366,7 +368,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl } else if (type == LocalAction.JOIN) { return engine.onJoinAction(txn, session); } else if (type == LocalAction.LEAVE) { - return engine.onLeaveAction(txn, session); + return engine.onLeaveAction(txn, session, false); } else if (type == LocalAction.MEMBER_ADDED) { return engine.onMemberAddedAction(txn, session); } else { @@ -435,7 +437,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl return new GroupInvitationResponse(m, contactGroupId, meta.getTimestamp(), meta.isLocal(), meta.isRead(), status.isSent(), status.isSeen(), sessionId, accept, - meta.getPrivateGroupId(), meta.getAutoDeleteTimer()); + meta.getPrivateGroupId(), meta.getAutoDeleteTimer(), + meta.isAutoDecline()); } @Override @@ -813,7 +816,7 @@ class GroupInvitationManagerImpl extends ConversationClientImpl // decline invitee sessions waiting for a response before if (session.state instanceof InviteeState && session.state.isAwaitingResponse()) { - respondToInvitation(txn, c, entry.getKey(), false); + respondToInvitation(txn, c, entry.getKey(), false, true); } for (MessageId m : session.messages) { db.deleteMessage(txn, m); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java index 9b9661ea9..34111cf04 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidator.java @@ -110,8 +110,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } // Create the metadata BdfDictionary meta = messageEncoder.encodeMetadata(INVITE, - privateGroup.getId(), m.getTimestamp(), false, false, false, - false, false, timer); + privateGroup.getId(), m.getTimestamp(), timer); return new BdfMessageContext(meta); } @@ -132,8 +131,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } BdfDictionary meta = messageEncoder.encodeMetadata(JOIN, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, timer); + new GroupId(privateGroupId), m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -160,8 +158,7 @@ class GroupInvitationValidator extends BdfMessageValidator { } BdfDictionary meta = messageEncoder.encodeMetadata(LEAVE, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, timer); + new GroupId(privateGroupId), m.getTimestamp(), timer); if (previousMessageId == null) { return new BdfMessageContext(meta); } else { @@ -177,8 +174,8 @@ class GroupInvitationValidator extends BdfMessageValidator { byte[] privateGroupId = body.getRaw(1); checkLength(privateGroupId, UniqueId.LENGTH); BdfDictionary meta = messageEncoder.encodeMetadata(ABORT, - new GroupId(privateGroupId), m.getTimestamp(), false, false, - false, false, false, NO_AUTO_DELETE_TIMER); + new GroupId(privateGroupId), m.getTimestamp(), + NO_AUTO_DELETE_TIMER); return new BdfMessageContext(meta); } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java index d7fdd070b..5afe1c443 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngine.java @@ -89,8 +89,8 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { } @Override - public InviteeSession onLeaveAction(Transaction txn, InviteeSession s) - throws DbException { + public InviteeSession onLeaveAction(Transaction txn, InviteeSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case LEFT: @@ -98,7 +98,7 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { case ERROR: return s; // Ignored in these states case INVITED: - return onLocalDecline(txn, s); + return onLocalDecline(txn, s, isAutoDecline); case ACCEPTED: case JOINED: return onLocalLeave(txn, s); @@ -203,14 +203,14 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { s.getInviteTimestamp(), ACCEPTED); } - private InviteeSession onLocalDecline(Transaction txn, InviteeSession s) - throws DbException { + private InviteeSession onLocalDecline(Transaction txn, InviteeSession s, + boolean isAutoDecline) throws DbException { // Mark the invite message unavailable to answer MessageId inviteId = s.getLastRemoteMessageId(); if (inviteId == null) throw new IllegalStateException(); markMessageAvailableToAnswer(txn, inviteId, false); // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, true); + Message sent = sendLeaveMessage(txn, s, true, isAutoDecline); // Track the message messageTracker.trackOutgoingMessage(txn, sent); // Move to the START state @@ -222,7 +222,7 @@ class InviteeProtocolEngine extends AbstractProtocolEngine { private InviteeSession onLocalLeave(Transaction txn, InviteeSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java index 7f65a8c42..72e8b91d6 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoder.java @@ -14,7 +14,11 @@ interface MessageEncoder { BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, - boolean available, boolean accepted, long autoDeleteTimer); + boolean available, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline); + + BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, + long timestamp, long autoDeleteTimer); void setVisibleInUi(BdfDictionary meta, boolean visible); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java index 8027ebf07..2ef288951 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageEncoderImpl.java @@ -20,6 +20,7 @@ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_INVITATION_ACCEPTED; +import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID; @@ -48,7 +49,7 @@ class MessageEncoderImpl implements MessageEncoder { public BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, boolean available, boolean accepted, - long autoDeleteTimer) { + long autoDeleteTimer, boolean isAutoDecline) { BdfDictionary meta = new BdfDictionary(); meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue()); meta.put(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId); @@ -61,9 +62,19 @@ class MessageEncoderImpl implements MessageEncoder { if (autoDeleteTimer != NO_AUTO_DELETE_TIMER) { meta.put(MSG_KEY_AUTO_DELETE_TIMER, autoDeleteTimer); } + if (isAutoDecline) { + meta.put(MSG_KEY_IS_AUTO_DECLINE, isAutoDecline); + } return meta; } + @Override + public BdfDictionary encodeMetadata(MessageType type, + GroupId privateGroupId, long timestamp, long autoDeleteTimer) { + return encodeMetadata(type, privateGroupId, timestamp, false, false, + false, false, false, autoDeleteTimer, false); + } + @Override public void setVisibleInUi(BdfDictionary meta, boolean visible) { meta.put(MSG_KEY_VISIBLE_IN_UI, visible); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java index f847a98fd..51812f86d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageMetadata.java @@ -13,10 +13,12 @@ class MessageMetadata { private final GroupId privateGroupId; private final long timestamp, autoDeleteTimer; private final boolean local, read, visible, available, accepted; + private final boolean isAutoDecline; MessageMetadata(MessageType type, GroupId privateGroupId, long timestamp, boolean local, boolean read, boolean visible, - boolean available, boolean accepted, long autoDeleteTimer) { + boolean available, boolean accepted, long autoDeleteTimer, + boolean isAutoDecline) { this.privateGroupId = privateGroupId; this.type = type; this.timestamp = timestamp; @@ -26,6 +28,7 @@ class MessageMetadata { this.available = available; this.accepted = accepted; this.autoDeleteTimer = autoDeleteTimer; + this.isAutoDecline = isAutoDecline; } MessageType getMessageType() { @@ -58,7 +61,7 @@ class MessageMetadata { /** * Returns true if the invitation was accepted. - * + *

* Only applies to messages of type INVITE. */ public boolean wasAccepted() { @@ -68,4 +71,8 @@ class MessageMetadata { public long getAutoDeleteTimer() { return autoDeleteTimer; } + + public boolean isAutoDecline() { + return isAutoDecline; + } } diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java index 0fdc19c8b..aac5ec988 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/MessageParserImpl.java @@ -23,6 +23,7 @@ import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AUTO_DELETE_TIMER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_INVITATION_ACCEPTED; +import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_IS_AUTO_DECLINE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE; import static org.briarproject.briar.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID; @@ -82,8 +83,9 @@ class MessageParserImpl implements MessageParser { boolean accepted = meta.getBoolean(MSG_KEY_INVITATION_ACCEPTED, false); long timer = meta.getLong(MSG_KEY_AUTO_DELETE_TIMER, NO_AUTO_DELETE_TIMER); + boolean isAutoDecline = meta.getBoolean(MSG_KEY_IS_AUTO_DECLINE, false); return new MessageMetadata(type, privateGroupId, timestamp, local, read, - visible, available, accepted, timer); + visible, available, accepted, timer, isAutoDecline); } @Override diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java index a8c00fd25..626cb6093 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngine.java @@ -85,8 +85,8 @@ class PeerProtocolEngine extends AbstractProtocolEngine { } @Override - public PeerSession onLeaveAction(Transaction txn, PeerSession s) - throws DbException { + public PeerSession onLeaveAction(Transaction txn, PeerSession s, + boolean isAutoDecline) throws DbException { switch (s.getState()) { case START: case AWAIT_MEMBER: @@ -213,7 +213,7 @@ class PeerProtocolEngine extends AbstractProtocolEngine { private PeerSession onLocalLeaveFromBothJoined(Transaction txn, PeerSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); @@ -229,7 +229,7 @@ class PeerProtocolEngine extends AbstractProtocolEngine { private PeerSession onLocalLeaveFromLocalJoined(Transaction txn, PeerSession s) throws DbException { // Send a LEAVE message - Message sent = sendLeaveMessage(txn, s, false); + Message sent = sendLeaveMessage(txn, s); try { // Make the private group invisible to the contact setPrivateGroupVisibility(txn, s, INVISIBLE); diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java index ec4010ff7..d44359d89 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/ProtocolEngine.java @@ -16,7 +16,14 @@ interface ProtocolEngine> { S onJoinAction(Transaction txn, S session) throws DbException; - S onLeaveAction(Transaction txn, S session) throws DbException; + /** + * Leaves the group or declines an invitation. + * + * @param isAutoDecline true if automatically declined due to deletion + * and false if initiated by the user. + */ + S onLeaveAction(Transaction txn, S session, boolean isAutoDecline) + throws DbException; S onMemberAddedAction(Transaction txn, S session) throws DbException; diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java index 1978d2b7f..b106ec10b 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngineTest.java @@ -194,7 +194,7 @@ abstract class AbstractProtocolEngineTest extends BrambleMockTestCase { context.checking(new Expectations() {{ oneOf(messageEncoder).encodeMetadata(type, privateGroupId, message.getTimestamp(), true, true, visible, false, false, - NO_AUTO_DELETE_TIMER); + NO_AUTO_DELETE_TIMER, false); will(returnValue(meta)); oneOf(clientHelper).addLocalMessage(txn, message, meta, true, false); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java index 633a55f89..b625f3192 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/AutoDeleteIntegrationTest.java @@ -5,7 +5,6 @@ import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient; -import org.briarproject.briar.api.conversation.ConversationMessageHeader; import org.briarproject.briar.api.privategroup.GroupMessage; import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroupManager; @@ -17,8 +16,6 @@ import org.briarproject.briar.test.BriarIntegrationTestComponent; import org.junit.Before; import org.junit.Test; -import java.util.List; - import javax.annotation.Nullable; import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS; @@ -28,6 +25,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { @@ -104,10 +102,9 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); // 1 marks the message as read - this starts 1's timer - final MessageId messageId = + final MessageId messageId0 = getMessageHeaders(c1, contactId0From1).get(0).getId(); - markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); + markMessageRead(c1, contact0From1, messageId0); assertGroupCount(c1, contactId0From1, 1, 0); // Before 1's timer elapses, 1 should still see the message @@ -127,9 +124,10 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertGroupCount(c1, contactId0From1, 1, 0); forEachHeader(c1, contactId0From1, 1, h -> { // The only message is not the same as before, but declined response - assertNotEquals(messageId, h.getId()); + assertNotEquals(messageId0, h.getId()); assertTrue(h instanceof GroupInvitationResponse); assertFalse(((GroupInvitationResponse) h).wasAccepted()); + assertTrue(((GroupInvitationResponse) h).isAutoDecline()); // The auto-decline message should have the expected timer assertEquals(MIN_AUTO_DELETE_TIMER_MS, h.getAutoDeleteTimer()); @@ -162,10 +160,9 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); // 0 marks the message as read - this starts 0's timer - MessageId messageId0 = + MessageId messageId1 = getMessageHeaders(c0, contactId1From0).get(0).getId(); - markMessageRead(c0, contact1From0, messageId0); - waitForEvents(c0); + markMessageRead(c0, contact1From0, messageId1); assertGroupCount(c0, contactId1From0, 1, 0); // Before 0's timer elapses, 0 should still see the message @@ -174,7 +171,7 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); // When 0's timer has elapsed, the message should be deleted from 0's - // view of the conversation but 1 should still see the message + // view of the conversation c0.getTimeTravel().addCurrentTimeMillis(1); assertGroupCount(c0, contactId1From0, 0, 0); assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); @@ -196,6 +193,7 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { sendInvitation(pg, contact1From0.getId(), null); sync0To1(1, true); ack1To0(1); + waitForEvents(c0); // The message should have been added the views of the conversation assertGroupCount(c0, contactId1From0, 1, 0); @@ -239,25 +237,24 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertGroupCount(c0, contactId1From0, 1, 0); assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); assertGroupCount(c1, contactId0From1, 2, 2); - List headers1 = - getMessageHeaders(c1, contactId0From1); - assertEquals(2, headers1.size()); + assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); - // 1 marks the message as read - this starts 1's timer - ConversationMessageHeader header = headers1.get(1); // newer message - assertEquals(privateGroup, - ((GroupInvitationRequest) header).getNameable()); - MessageId messageId = header.getId(); - markMessageRead(c1, contact0From1, messageId); - waitForEvents(c1); - assertGroupCount(c1, contactId0From1, 2, 1); + // 1 marks all the message as read - this starts 1's timer for 2nd msg + forEachHeader(c1, contactId0From1, 2, h -> { + try { + markMessageRead(c1, contact0From1, h.getId()); + } catch (Exception e) { + fail(); + } + }); + assertGroupCount(c1, contactId0From1, 2, 0); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); assertGroupCount(c0, contactId1From0, 1, 0); assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); - assertGroupCount(c1, contactId0From1, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 0); assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); // When 1's timer has elapsed, the message should be deleted from 1's @@ -266,7 +263,8 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { c1.getTimeTravel().addCurrentTimeMillis(1); assertGroupCount(c0, contactId1From0, 1, 0); assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); - assertGroupCount(c1, contactId0From1, 2, 1); + // 1's total count is still 2, because of the added auto-decline + assertGroupCount(c1, contactId0From1, 2, 0); forEachHeader(c1, contactId0From1, 2, h -> { if (h instanceof GroupInvitationRequest) { // the request is for the first invitation @@ -277,6 +275,7 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { GroupInvitationResponse r = (GroupInvitationResponse) h; // is auto-decline for 2nd invitation assertEquals(privateGroup.getId(), r.getShareableId()); + assertTrue(r.isAutoDecline()); assertFalse(r.wasAccepted()); } }); @@ -288,34 +287,48 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { waitForEvents(c1); // 0 marks the message as read - this starts 0's timer GroupInvitationResponse autoDeclineMessage = (GroupInvitationResponse) - getMessageHeaders(c1, contactId0From1).get(1); + getMessageHeaders(c0, contactId1From0).get(1); markMessageRead(c0, contact1From0, autoDeclineMessage.getId()); - waitForEvents(c0); assertGroupCount(c0, contactId1From0, 2, 0); - assertGroupCount(c1, contactId0From1, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 0); // Timer of auto-decline elapses for both peers at the same time c0.getTimeTravel().addCurrentTimeMillis(timerLatency); c1.getTimeTravel().addCurrentTimeMillis(timerLatency); assertGroupCount(c0, contactId1From0, 1, 0); - assertGroupCount(c1, contactId0From1, 1, 1); + assertGroupCount(c1, contactId0From1, 1, 0); // 1 responds to first invitation (that had no timer) groupInvitationManager1.respondToInvitation(contactId0From1, pg, true); // Sync the accept response message to 0 sync1To0(1, true); - // Sync the ack to 1 - this starts 1's timer - ack0To1(1); + // Sync the ack (and creator's join messages (group + protocol) to 1 + // this starts 1's timer + sync0To1(2, true); waitForEvents(c1); assertGroupCount(c0, contactId1From0, 2, 1); - assertGroupCount(c1, contactId0From1, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 0); + forEachHeader(c1, contactId0From1, 2, h -> { + if (h instanceof GroupInvitationRequest) { + // the request is for the first invitation + assertEquals(pg.getId(), + ((GroupInvitationRequest) h).getNameable().getId()); + } else { + assertTrue(h instanceof GroupInvitationResponse); + GroupInvitationResponse r = (GroupInvitationResponse) h; + // is accept for 1nd invitation + assertEquals(pg.getId(), r.getShareableId()); + assertFalse(r.isAutoDecline()); + assertTrue(r.wasAccepted()); + } + }); // Before 1's timer elapses, 1 should still see the message c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); assertGroupCount(c0, contactId1From0, 2, 1); assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); - assertGroupCount(c1, contactId0From1, 2, 1); + assertGroupCount(c1, contactId0From1, 2, 0); assertEquals(2, getMessageHeaders(c1, contactId0From1).size()); // When 1's timer has elapsed, the message should be deleted from 1's @@ -324,12 +337,43 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { c1.getTimeTravel().addCurrentTimeMillis(1); assertGroupCount(c0, contactId1From0, 2, 1); assertEquals(2, getMessageHeaders(c0, contactId1From0).size()); - assertGroupCount(c1, contactId0From1, 1, 1); + assertGroupCount(c1, contactId0From1, 1, 0); forEachHeader(c1, contactId0From1, 1, h -> { assertTrue(h instanceof GroupInvitationRequest); assertTrue(((GroupInvitationRequest) h).wasAnswered()); assertTrue(((GroupInvitationRequest) h).canBeOpened()); }); + + // 0 reads all messages + forEachHeader(c0, contactId1From0, 2, h -> { + try { + if (!h.isRead()) markMessageRead(c0, contact1From0, h.getId()); + } catch (Exception e) { + fail(); + } + }); + assertGroupCount(c0, contactId1From0, 2, 0); + + // Before 0's timer elapses, 0 should still see the messages + c0.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency - 1); + assertGroupCount(c0, contactId1From0, 2, 0); + assertGroupCount(c1, contactId0From1, 1, 0); + + // When 0's timer has elapsed, the messages should be deleted from 0's + // view of the conversation, only the initial invitation remains + c0.getTimeTravel().addCurrentTimeMillis(1); + c1.getTimeTravel().addCurrentTimeMillis(1); + assertGroupCount(c0, contactId1From0, 1, 0); + assertEquals(1, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 1, 0); + assertEquals(1, getMessageHeaders(c1, contactId0From1).size()); + + // 1 joined the PrivateGroup + assertEquals(pg, + c1.getPrivateGroupManager().getPrivateGroup(pg.getId())); + assertFalse(groupInvitationManager0 + .isInvitationAllowed(contact1From0, pg.getId())); } @Test @@ -363,8 +407,8 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { // 1 responds to invitation groupInvitationManager1 - .respondToInvitation(contactId0From1, privateGroup, true); - // Sync the accept response message to 0 + .respondToInvitation(contactId0From1, privateGroup, false); + // Sync the decline response message to 0 sync1To0(1, true); // Sync the ack to 1 - this starts 1's timer ack0To1(1); @@ -373,18 +417,19 @@ public class AutoDeleteIntegrationTest extends AbstractAutoDeleteTest { assertGroupCount(c1, contactId0From1, 2, 0); // 0 marks the message as read - this starts 0's timer - GroupInvitationResponse autoDeclineMessage = (GroupInvitationResponse) - getMessageHeaders(c1, contactId0From1).get(1); - markMessageRead(c0, contact1From0, autoDeclineMessage.getId()); - waitForEvents(c0); + GroupInvitationResponse message1 = (GroupInvitationResponse) + getMessageHeaders(c0, contactId1From0).get(0); + markMessageRead(c0, contact1From0, message1.getId()); assertGroupCount(c0, contactId1From0, 1, 0); assertGroupCount(c1, contactId0From1, 2, 0); - // 1 joined the PrivateGroup - assertEquals(privateGroup, c1.getPrivateGroupManager() - .getPrivateGroup(privateGroup.getId())); - assertFalse(groupInvitationManager0 - .isInvitationAllowed(contact1From0, privateGroup.getId())); + // both peers delete all messages after their timers expire + c0.getTimeTravel().addCurrentTimeMillis(timerLatency); + c1.getTimeTravel().addCurrentTimeMillis(timerLatency); + assertGroupCount(c0, contactId1From0, 0, 0); + assertEquals(0, getMessageHeaders(c0, contactId1From0).size()); + assertGroupCount(c1, contactId0From1, 0, 0); + assertEquals(0, getMessageHeaders(c1, contactId0From1).size()); } private PrivateGroup addPrivateGroup(String name, long timestamp) diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java index 4a3e7fb85..e47642e2e 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/CreatorProtocolEngineTest.java @@ -123,19 +123,19 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { CreatorSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromDissolved() throws Exception { CreatorSession session = getDefaultSession(DISSOLVED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { CreatorSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -143,7 +143,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(INVITED); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); @@ -157,7 +157,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(JOINED); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); @@ -171,7 +171,7 @@ public class CreatorProtocolEngineTest extends AbstractProtocolEngineTest { CreatorSession session = getDefaultSession(LEFT); expectOnLocalLeave(); - CreatorSession newSession = engine.onLeaveAction(txn, session); + CreatorSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(DISSOLVED, newSession.getState()); assertEquals(messageId, newSession.getLastLocalMessageId()); assertEquals(lastRemoteMessageId, newSession.getLastRemoteMessageId()); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java index 01a3e79a3..7b873d897 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImplTest.java @@ -593,7 +593,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { .parseInviteeSession(contactGroup.getId(), bdfSession); will(returnValue(inviteeSession)); if (accept) oneOf(inviteeEngine).onJoinAction(txn, inviteeSession); - else oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession); + else oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession, false); will(returnValue(inviteeSession)); }}); expectStoreSession(inviteeSession, storageMessage.getId()); @@ -653,10 +653,10 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { long time1 = 1L, time2 = 2L; MessageMetadata messageMetadata1 = new MessageMetadata(INVITE, privateGroup.getId(), time1, true, - true, true, false, true, NO_AUTO_DELETE_TIMER); + true, true, false, true, NO_AUTO_DELETE_TIMER, false); MessageMetadata messageMetadata2 = new MessageMetadata(JOIN, privateGroup.getId(), time2, true, - true, true, true, false, NO_AUTO_DELETE_TIMER); + true, true, true, false, NO_AUTO_DELETE_TIMER, false); InviteMessage invite = new InviteMessage(message.getId(), contactGroup.getId(), privateGroup.getId(), time1, "name", author, @@ -876,7 +876,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parseCreatorSession(contactGroup.getId(), bdfSession); will(returnValue(creatorSession)); - oneOf(creatorEngine).onLeaveAction(txn, creatorSession); + oneOf(creatorEngine).onLeaveAction(txn, creatorSession, false); will(returnValue(creatorSession)); // session 2 oneOf(sessionParser).getRole(bdfSession2); @@ -884,7 +884,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parseInviteeSession(contactGroup2.getId(), bdfSession2); will(returnValue(inviteeSession)); - oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession); + oneOf(inviteeEngine).onLeaveAction(txn, inviteeSession, false); will(returnValue(inviteeSession)); // session 3 oneOf(sessionParser).getRole(bdfSession3); @@ -892,7 +892,7 @@ public class GroupInvitationManagerImplTest extends BrambleMockTestCase { oneOf(sessionParser) .parsePeerSession(contactGroup3.getId(), bdfSession3); will(returnValue(peerSession)); - oneOf(peerEngine).onLeaveAction(txn, peerSession); + oneOf(peerEngine).onLeaveAction(txn, peerSession, false); will(returnValue(peerSession)); }}); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java index fd5ccbe1d..487380bdf 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/GroupInvitationValidatorTest.java @@ -636,8 +636,7 @@ public class GroupInvitationValidatorTest extends ValidatorTestCase { long autoDeleteTimer, BdfDictionary metadata) { context.checking(new Expectations() {{ oneOf(messageEncoder).encodeMetadata(type, message.getGroupId(), - message.getTimestamp(), false, false, false, false, false, - autoDeleteTimer); + message.getTimestamp(), autoDeleteTimer); will(returnValue(metadata)); }}); } diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java index 43e63f34f..47a749844 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/InviteeProtocolEngineTest.java @@ -193,25 +193,25 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { InviteeSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromLeft() throws Exception { InviteeSession session = getDefaultSession(LEFT); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromDissolved() throws Exception { InviteeSession session = getDefaultSession(DISSOLVED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { InviteeSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -223,7 +223,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { }}); InviteeSession session = getDefaultSession(INVITED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(START, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -245,7 +245,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); InviteeSession session = getDefaultSession(ACCEPTED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -257,7 +257,7 @@ public class InviteeProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); InviteeSession session = getDefaultSession(JOINED); - InviteeSession newSession = engine.onLeaveAction(txn, session); + InviteeSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java index 69383ba38..6118dd32c 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/invitation/PeerProtocolEngineTest.java @@ -145,31 +145,31 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { @Test public void testOnLeaveActionFromStart() throws Exception { PeerSession session = getDefaultSession(START); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromAwaitMember() throws Exception { PeerSession session = getDefaultSession(AWAIT_MEMBER); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromNeitherJoined() throws Exception { PeerSession session = getDefaultSession(NEITHER_JOINED); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromLocalLeft() throws Exception { PeerSession session = getDefaultSession(LOCAL_LEFT); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test public void testOnLeaveActionFromError() throws Exception { PeerSession session = getDefaultSession(ERROR); - assertEquals(session, engine.onLeaveAction(txn, session)); + assertEquals(session, engine.onLeaveAction(txn, session, false)); } @Test @@ -178,7 +178,7 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); - PeerSession newSession = engine.onLeaveAction(txn, session); + PeerSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(NEITHER_JOINED, newSession.getState()); assertSessionRecordedSentMessage(newSession); @@ -191,7 +191,7 @@ public class PeerProtocolEngineTest extends AbstractProtocolEngineTest { expectSendLeaveMessage(false); expectSetPrivateGroupVisibility(INVISIBLE); - PeerSession newSession = engine.onLeaveAction(txn, session); + PeerSession newSession = engine.onLeaveAction(txn, session, false); assertEquals(LOCAL_LEFT, newSession.getState()); assertSessionRecordedSentMessage(newSession); From 64e3940f77a0556680011cc793a8afff2126a539 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 2 Mar 2021 11:13:48 -0300 Subject: [PATCH 5/6] Render automatic declines differently in the UI and show them as they happen via an Event --- .../android/conversation/ConversationVisitor.java | 4 ++++ briar-android/src/main/res/values/strings.xml | 1 + .../invitation/AbstractProtocolEngine.java | 15 +++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java index 756c8535e..0b9b48bd9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationVisitor.java @@ -209,6 +209,10 @@ class ConversationVisitor implements text = ctx.getString( R.string.groups_invitations_response_accepted_sent, contactName.getValue()); + } else if (r.isAutoDecline()) { + text = ctx.getString( + R.string.groups_invitations_response_declined_auto, + contactName.getValue()); } else { text = ctx.getString( R.string.groups_invitations_response_declined_sent, diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index f138d30ae..d9eec9975 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -365,6 +365,7 @@ You accepted the group invitation from %s. You declined the group invitation from %s. + The group invitation from %s was automatically declined. %s accepted the group invitation. %s declined the group invitation. Only the creator can invite new members to the group. Below are all current members of the group. diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java index 9efabfa1f..906f4abc8 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/AbstractProtocolEngine.java @@ -7,6 +7,7 @@ import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.identity.IdentityManager; import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; @@ -19,13 +20,16 @@ import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.api.versioning.ClientVersioningManager; import org.briarproject.briar.api.autodelete.AutoDeleteManager; import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.conversation.ConversationManager; import org.briarproject.briar.api.privategroup.GroupMessage; import org.briarproject.briar.api.privategroup.GroupMessageFactory; import org.briarproject.briar.api.privategroup.PrivateGroup; import org.briarproject.briar.api.privategroup.PrivateGroupFactory; import org.briarproject.briar.api.privategroup.PrivateGroupManager; +import org.briarproject.briar.api.privategroup.event.GroupInvitationResponseReceivedEvent; import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager; +import org.briarproject.briar.api.privategroup.invitation.GroupInvitationResponse; import java.util.Collection; @@ -208,6 +212,17 @@ abstract class AbstractProtocolEngine> if (timer != NO_AUTO_DELETE_TIMER) { db.setCleanupTimerDuration(txn, m.getId(), timer); } + if (isAutoDecline) { + // Broadcast an event, so the auto-decline becomes visible + SessionId sessionId = + new SessionId(s.getPrivateGroupId().getBytes()); + GroupInvitationResponse response = new GroupInvitationResponse( + m.getId(), s.getContactGroupId(), m.getTimestamp(), + true, true, false, false, sessionId, false, + s.getPrivateGroupId(), timer, true); + Event e = new GroupInvitationResponseReceivedEvent(response, c); + txn.attach(e); + } } else { m = messageEncoder.encodeLeaveMessage(s.getContactGroupId(), s.getPrivateGroupId(), localTimestamp, From dde94baebdde8acff31b192dcd68a182f24dc8e5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 3 Mar 2021 11:32:38 -0300 Subject: [PATCH 6/6] Use stored session metadata instead of fetching it again --- .../privategroup/invitation/GroupInvitationManagerImpl.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java index acecd6038..ece8ed827 100644 --- a/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/privategroup/invitation/GroupInvitationManagerImpl.java @@ -797,10 +797,8 @@ class GroupInvitationManagerImpl extends ConversationClientImpl if (deletableSession == null) { StoredSession ss = getSession(txn, g, sessionId); if (ss == null) throw new DbException(); - BdfDictionary sessionMeta = clientHelper - .getMessageMetadataAsDictionary(txn, ss.storageId); - Session session = sessionParser - .parseSession(g, sessionMeta); + Session session = + sessionParser.parseSession(g, ss.bdfSession); deletableSession = new DeletableSession(session.getState()); sessions.put(sessionId, deletableSession); }