diff --git a/.idea/dictionaries/briar.xml b/.idea/dictionaries/briar.xml
index 71bcd22b4..7eb591723 100644
--- a/.idea/dictionaries/briar.xml
+++ b/.idea/dictionaries/briar.xml
@@ -7,6 +7,7 @@
>
m = messageEncoder.encodeRequestMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), author, text, timer);
sendMessage(txn, REQUEST, s.getSessionId(), m, true, timer);
+ // Set the auto-delete timer duration on the local message
+ if (timer != NO_AUTO_DELETE_TIMER) {
+ db.setCleanupTimerDuration(txn, m.getId(), timer);
+ }
} else {
m = messageEncoder.encodeRequestMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), author, text);
@@ -128,6 +134,10 @@ abstract class AbstractProtocolEngine>
ephemeralPublicKey, acceptTimestamp, transportProperties,
timer);
sendMessage(txn, ACCEPT, s.getSessionId(), m, visible, 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.encodeAcceptMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId(),
@@ -139,7 +149,8 @@ abstract class AbstractProtocolEngine>
}
Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp,
- boolean visible) throws DbException {
+ boolean visible, boolean isAutoDecline) throws DbException {
+ if (!visible && isAutoDecline) throw new IllegalArgumentException();
Message m;
ContactId c = getContactId(txn, s.getContactGroupId());
if (contactSupportsAutoDeletion(txn, c)) {
@@ -148,7 +159,25 @@ abstract class AbstractProtocolEngine>
m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId(),
timer);
- sendMessage(txn, DECLINE, s.getSessionId(), m, visible, timer);
+ sendMessage(txn, DECLINE, s.getSessionId(), m, visible, timer,
+ isAutoDecline);
+ // Set the auto-delete timer duration on the local message
+ if (timer != NO_AUTO_DELETE_TIMER) {
+ db.setCleanupTimerDuration(txn, m.getId(), timer);
+ }
+ if (isAutoDecline) {
+ // Broadcast an event, so the auto-decline becomes visible
+ IntroduceeSession session = (IntroduceeSession) s;
+ Author author = session.getRemote().author;
+ AuthorInfo authorInfo =
+ authorManager.getAuthorInfo(txn, author.getId());
+ IntroductionResponse response = new IntroductionResponse(
+ m.getId(), s.getContactGroupId(), m.getTimestamp(),
+ true, true, false, false, s.getSessionId(), false,
+ author, authorInfo, INTRODUCEE, false, timer, true);
+ Event e = new IntroductionResponseReceivedEvent(response, c);
+ txn.attach(e);
+ }
} else {
m = messageEncoder.encodeDeclineMessage(s.getContactGroupId(),
timestamp, s.getLastLocalMessageId(), s.getSessionId());
@@ -192,9 +221,16 @@ abstract class AbstractProtocolEngine>
private void sendMessage(Transaction txn, MessageType type,
SessionId sessionId, Message m, boolean visibleInConversation,
long autoDeleteTimer) throws DbException {
+ sendMessage(txn, type, sessionId, m, visibleInConversation,
+ autoDeleteTimer, false);
+ }
+
+ private void sendMessage(Transaction txn, MessageType type,
+ SessionId sessionId, Message m, boolean visibleInConversation,
+ long autoDeleteTimer, boolean isAutoDecline) throws DbException {
BdfDictionary meta = messageEncoder.encodeMetadata(type, sessionId,
m.getTimestamp(), true, true, visibleInConversation,
- autoDeleteTimer);
+ autoDeleteTimer, isAutoDecline);
try {
clientHelper.addLocalMessage(txn, m, meta, true, false);
} catch (FormatException e) {
@@ -215,7 +251,7 @@ abstract class AbstractProtocolEngine>
m.getTimestamp(), false, false, false, false,
s.getSessionId(), m instanceof AcceptMessage,
otherAuthor, otherAuthorInfo, s.getRole(), canSucceed,
- m.getAutoDeleteTimer());
+ m.getAutoDeleteTimer(), false);
IntroductionResponseReceivedEvent e =
new IntroductionResponseReceivedEvent(response, c.getId());
txn.attach(e);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index 9475f5bdf..7f55f1a2a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -123,12 +123,13 @@ class IntroduceeProtocolEngine
@Override
public IntroduceeSession onDeclineAction(Transaction txn,
- IntroduceeSession session) throws DbException {
+ IntroduceeSession session, boolean isAutoDecline)
+ throws DbException {
switch (session.getState()) {
case AWAIT_RESPONSES:
case REMOTE_DECLINED:
case REMOTE_ACCEPTED:
- return onLocalDecline(txn, session);
+ return onLocalDecline(txn, session, isAutoDecline);
case START:
case LOCAL_DECLINED:
case LOCAL_ACCEPTED:
@@ -319,13 +320,14 @@ class IntroduceeProtocolEngine
}
private IntroduceeSession onLocalDecline(Transaction txn,
- IntroduceeSession s) throws DbException {
+ IntroduceeSession s, boolean isAutoDecline) throws DbException {
// Mark the request message unavailable to answer
markRequestsUnavailableToAnswer(txn, s);
// Send a DECLINE message
long localTimestamp = getTimestampForVisibleMessage(txn, s);
- Message sent = sendDeclineMessage(txn, s, localTimestamp, true);
+ Message sent =
+ sendDeclineMessage(txn, s, localTimestamp, true, isAutoDecline);
// Track the message
messageTracker.trackOutgoingMessage(txn, sent);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
index b0d1dbdd7..f300c2d04 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
@@ -98,7 +98,7 @@ class IntroducerProtocolEngine
@Override
public IntroducerSession onDeclineAction(Transaction txn,
- IntroducerSession s) {
+ IntroducerSession s, boolean isAutoDecline) {
throw new UnsupportedOperationException(); // Invalid in this role
}
@@ -387,7 +387,7 @@ class IntroducerProtocolEngine
Introducee i = getOtherIntroducee(s, m.getGroupId());
// The forwarded message will be visible to the introducee
long localTimestamp = getTimestampForVisibleMessage(txn, s, i);
- Message sent = sendDeclineMessage(txn, i, localTimestamp, false);
+ Message sent = sendDeclineMessage(txn, i, localTimestamp, false, false);
// Create the next state
IntroducerState state = START;
@@ -442,7 +442,7 @@ class IntroducerProtocolEngine
Introducee i = getOtherIntroducee(s, m.getGroupId());
// The forwarded message will be visible to the introducee
long localTimestamp = getTimestampForVisibleMessage(txn, s, i);
- Message sent = sendDeclineMessage(txn, i, localTimestamp, false);
+ Message sent = sendDeclineMessage(txn, i, localTimestamp, false, false);
Introducee introduceeA, introduceeB;
Author sender, other;
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
index 72a5066d7..37e914d36 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
@@ -10,6 +10,7 @@ interface IntroductionConstants {
String MSG_KEY_VISIBLE_IN_UI = "visibleInUi";
String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
String MSG_KEY_AUTO_DELETE_TIMER = "autoDeleteTimer";
+ String MSG_KEY_IS_AUTO_DECLINE = "isAutoDecline";
// Session Keys
String SESSION_KEY_SESSION_ID = "sessionId";
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
index df3a33d3b..6d3139ec8 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java
@@ -20,7 +20,7 @@ interface IntroductionCrypto {
/**
* Returns true if the local author is alice
- *
+ *
* Alice is the Author whose unique ID has the lower ID,
* comparing the IDs as byte strings.
*/
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index 8d8a68a7a..d52192b49 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -1,6 +1,7 @@
package org.briarproject.briar.introduction;
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;
@@ -28,6 +29,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;
@@ -54,8 +56,10 @@ import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.inject.Inject;
+import static org.briarproject.briar.api.autodelete.AutoDeleteConstants.NO_AUTO_DELETE_TIMER;
import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
+import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES;
import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_DECLINED;
import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED;
@@ -71,7 +75,7 @@ import static org.briarproject.briar.introduction.MessageType.REQUEST;
@NotNullByDefault
class IntroductionManagerImpl extends ConversationClientImpl
implements IntroductionManager, OpenDatabaseHook, ContactHook,
- ClientVersioningHook {
+ ClientVersioningHook, CleanupHook {
private final ClientVersioningManager clientVersioningManager;
private final ContactGroupFactory contactGroupFactory;
@@ -170,6 +174,11 @@ class IntroductionManagerImpl 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 = meta.getSessionId();
IntroduceeSession newIntroduceeSession = null;
@@ -362,7 +371,19 @@ class IntroductionManagerImpl extends ConversationClientImpl
@Override
public void respondToIntroduction(ContactId contactId, SessionId sessionId,
boolean accept) throws DbException {
- Transaction txn = db.startTransaction(false);
+ respondToIntroduction(contactId, sessionId, accept, false);
+ }
+
+ private void respondToIntroduction(ContactId contactId, SessionId sessionId,
+ boolean accept, boolean isAutoDecline) throws DbException {
+ db.transaction(false,
+ txn -> respondToIntroduction(txn, contactId, sessionId, accept,
+ isAutoDecline));
+ }
+
+ private void respondToIntroduction(Transaction txn, ContactId contactId,
+ SessionId sessionId, boolean accept, boolean isAutoDecline)
+ throws DbException {
try {
// Look up the session
StoredSession ss = getSession(txn, sessionId);
@@ -381,15 +402,13 @@ class IntroductionManagerImpl extends ConversationClientImpl
if (accept) {
session = introduceeEngine.onAcceptAction(txn, session);
} else {
- session = introduceeEngine.onDeclineAction(txn, session);
+ session = introduceeEngine
+ .onDeclineAction(txn, session, isAutoDecline);
}
// Store the updated session
storeSession(txn, ss.storageId, session);
- db.commitTransaction(txn);
} catch (FormatException e) {
throw new DbException(e);
- } finally {
- db.endTransaction(txn);
}
}
@@ -487,7 +506,7 @@ class IntroductionManagerImpl extends ConversationClientImpl
return new IntroductionResponse(m, contactGroupId, meta.getTimestamp(),
meta.isLocal(), meta.isRead(), status.isSent(), status.isSeen(),
sessionId, accept, author, authorInfo, role, canSucceed,
- meta.getAutoDeleteTimer());
+ meta.getAutoDeleteTimer(), meta.isAutoDecline());
}
private void removeSessionWithIntroducer(Transaction txn,
@@ -547,6 +566,67 @@ class IntroductionManagerImpl extends ConversationClientImpl
}
}
+ @Override
+ public void deleteMessages(Transaction txn, GroupId g,
+ Collection
+ * Let introductions self-destruct at the introducer only
+ * Variant 1: Let both introducees decline the introduction
+ * Variant 2: Let first introducee accept, second decline the introduction
+ * Variant 3: Let first introducee decline, second accept the introduction
+ * ASSERT that accept/decline messages still get forwarded to the other introducer
+ * ASSERT that the introduction does not succeed
+ * ASSERT that abort messages do get sent
+ * ASSERT all messages involved self-destruct eventually
+ * ASSERT that a new introduction can succeed afterwards
+ */
+
+ @Test
+ public void testFailAfterIntroducerSelfDestructedBothDecline()
+ throws Exception {
+ testFailAfterIntroducerSelfDestructed(false, false);
+ }
+
+ @Test
+ public void testFailAfterIntroducerSelfDestructedFirstAccept()
+ throws Exception {
+ testFailAfterIntroducerSelfDestructed(true, false);
+ }
+
+ @Test
+ public void testFailAfterIntroducerSelfDestructedSecondAccept()
+ throws Exception {
+ testFailAfterIntroducerSelfDestructed(false, true);
+ }
+
+ private void testFailAfterIntroducerSelfDestructed(boolean firstAccepts,
+ boolean secondAccepts) throws Exception {
+ assertFalse(firstAccepts && secondAccepts);
+
+ makeIntroduction(true, true);
+
+ // ack from 1 and 2 to 0. This starts 0's timer for 1 and 2
+ ack1To0(1);
+ ack2To0(1);
+ waitForEvents(c0);
+
+ // Travel in time at 0
+ timeTravel(c0);
+
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(0, 0);
+
+ // -> introductions self-destructed at the introducer only
+
+ assertIntroducerStatus(AWAIT_RESPONSES);
+ assertIntroduceeStatus(c1, IntroduceeState.AWAIT_RESPONSES);
+ assertIntroduceeStatus(c2, IntroduceeState.AWAIT_RESPONSES);
+
+ // introducees have got the introduction
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+
+ // first contact reads the introduction and responds
+ markMessagesRead(c1, contact0From1);
+ respondToMostRecentIntroduction(c1, contactId0From1, firstAccepts);
+ sync1To0(1, true);
+ waitForEvents(c0);
+
+ if (firstAccepts) {
+ assertIntroducerStatusFirstAccepted();
+ } else {
+ assertIntroducerStatusFirstDeclined();
+ }
+
+ // second contact reads the introduction and responds
+ markMessagesRead(c2, contact0From2);
+ respondToMostRecentIntroduction(c2, contactId0From2, secondAccepts);
+ sync2To0(1, true);
+ waitForEvents(c0);
+
+ assertIntroducerStatus(START);
+
+ // introducees have got the own response
+ assertGroupCountAt1With0(2, 0);
+ assertGroupCountAt2With0(2, 0);
+
+ // sync forwarded ACCEPT/DECLINE messages to introducees
+ sync0To1(1, true);
+ waitForEvents(c1);
+ sync0To2(1, true);
+ waitForEvents(c2);
+
+ assertIntroductionFailed();
+
+ if (firstAccepts) {
+ // one additional message, the other introducee's response
+ assertGroupCountAt1With0(3, 1);
+ } else {
+ assertGroupCountAt1With0(2, 0);
+ }
+ if (secondAccepts) {
+ // one additional message, the other introducee's response
+ assertGroupCountAt2With0(3, 1);
+ } else {
+ assertGroupCountAt2With0(2, 0);
+ }
+
+ timeTravel(c1);
+ timeTravel(c2);
+
+ if (firstAccepts) {
+ assertGroupCountAt1With0(1, 1);
+ } else {
+ assertGroupCountAt1With0(0, 0);
+ }
+ if (secondAccepts) {
+ assertGroupCountAt2With0(1, 1);
+ } else {
+ assertGroupCountAt2With0(0, 0);
+ }
+
+ // -> if one of the introducees accepted, they still have got an unread
+ // decline from the other introducee
+
+ if (firstAccepts) {
+ markMessagesRead(c1, contact0From1);
+ timeTravel(c1);
+ assertGroupCountAt1With0(0, 0);
+ }
+ if (secondAccepts) {
+ markMessagesRead(c2, contact0From2);
+ timeTravel(c2);
+ assertGroupCountAt2With0(0, 0);
+ }
+
+ // make sure the introducees session status returned to START
+ assertIntroduceeStatus(c1, IntroduceeState.START);
+ assertIntroduceeStatus(c2, IntroduceeState.START);
+
+ assertNewIntroductionSucceeds();
+ }
+
+ private void makeIntroduction(boolean enableTimer1, boolean enableTimer2)
+ throws Exception {
+ if (enableTimer1) {
+ setAutoDeleteTimer(c0, contact1From0.getId(),
+ MIN_AUTO_DELETE_TIMER_MS);
+ }
+ if (enableTimer2) {
+ setAutoDeleteTimer(c0, contact2From0.getId(),
+ MIN_AUTO_DELETE_TIMER_MS);
+ }
+
+ // make introduction
+ c0.getIntroductionManager()
+ .makeIntroduction(contact1From0, contact2From0, "Hi!");
+
+ sync0To1(1, true);
+ sync0To2(1, true);
+ waitForEvents(c1);
+ waitForEvents(c2);
+ }
+
+ private void respondToMostRecentIntroduction(
+ BriarIntegrationTestComponent c, ContactId contactId,
+ boolean accept) throws Exception {
+ List> {
S onAcceptAction(Transaction txn, S session) throws DbException;
- S onDeclineAction(Transaction txn, S session) throws DbException;
+ /**
+ * Declines an introduction.
+ *
+ * @param isAutoDecline true if automatically declined due to deletion
+ * and false if initiated by the user.
+ */
+ S onDeclineAction(Transaction txn, S session, boolean isAutoDecline)
+ throws DbException;
S onRequestMessage(Transaction txn, S session, RequestMessage m)
throws DbException, FormatException;
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
index 6f1081791..be35939f3 100644
--- a/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/autodelete/AbstractAutoDeleteTest.java
@@ -102,7 +102,7 @@ public abstract class AbstractAutoDeleteTest extends
@FunctionalInterface
protected interface HeaderConsumer {
- void accept(ConversationMessageHeader header);
+ void accept(ConversationMessageHeader header) throws DbException;
}
protected void forEachHeader(BriarIntegrationTestComponent component,
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java
new file mode 100644
index 000000000..5ff0220d0
--- /dev/null
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/AutoDeleteIntegrationTest.java
@@ -0,0 +1,968 @@
+package org.briarproject.briar.introduction;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.contact.Contact;
+import org.briarproject.bramble.api.contact.ContactId;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.db.DbException;
+import org.briarproject.bramble.api.properties.TransportPropertyManager;
+import org.briarproject.bramble.api.sync.Group;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.system.TimeTravelModule;
+import org.briarproject.bramble.test.TestDatabaseConfigModule;
+import org.briarproject.briar.api.conversation.ConversationManager.ConversationClient;
+import org.briarproject.briar.api.conversation.ConversationMessageHeader;
+import org.briarproject.briar.api.introduction.IntroductionRequest;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
+import org.briarproject.briar.autodelete.AbstractAutoDeleteTest;
+import org.briarproject.briar.test.BriarIntegrationTestComponent;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.briarproject.bramble.api.cleanup.CleanupManager.BATCH_DELAY_MS;
+import static org.briarproject.bramble.test.TestPluginConfigModule.SIMPLEX_TRANSPORT_ID;
+import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
+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.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionManager.MAJOR_VERSION;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSES;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_A;
+import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_B;
+import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.START;
+import static org.briarproject.briar.test.TestEventListener.assertEvent;
+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 AbstractAutoDeleteTest {
+
+ @Override
+ protected void createComponents() {
+ IntroductionIntegrationTestComponent component =
+ DaggerIntroductionIntegrationTestComponent.builder().build();
+ BriarIntegrationTestComponent.Helper.injectEagerSingletons(component);
+ component.inject(this);
+
+ IntroductionIntegrationTestComponent c0 =
+ DaggerIntroductionIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(
+ new TestDatabaseConfigModule(t0Dir))
+ .timeTravelModule(new TimeTravelModule(true))
+ .build();
+ BriarIntegrationTestComponent.Helper.injectEagerSingletons(c0);
+
+ IntroductionIntegrationTestComponent c1 =
+ DaggerIntroductionIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(
+ new TestDatabaseConfigModule(t1Dir))
+ .timeTravelModule(new TimeTravelModule(true))
+ .build();
+ BriarIntegrationTestComponent.Helper.injectEagerSingletons(c1);
+
+ IntroductionIntegrationTestComponent c2 =
+ DaggerIntroductionIntegrationTestComponent.builder()
+ .testDatabaseConfigModule(
+ new TestDatabaseConfigModule(t2Dir))
+ .timeTravelModule(new TimeTravelModule(true))
+ .build();
+ BriarIntegrationTestComponent.Helper.injectEagerSingletons(c2);
+
+ this.c0 = c0;
+ this.c1 = c1;
+ this.c2 = 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();
+
+ addTransportProperties();
+ }
+
+ @Override
+ protected ConversationClient getConversationClient(
+ BriarIntegrationTestComponent component) {
+ return component.getIntroductionManager();
+ }
+
+ /*
+ * Basic tests.
+ * ASSERT timers are set on introduction messages
+ * ASSERT introduction messages self-destruct on all three sides
+ */
+
+ @Test
+ public void testIntroductionMessagesHaveTimer() throws Exception {
+ makeIntroduction(true, true);
+ assertIntroductionsArrived();
+
+ assertMessagesAmong0And1HaveTimerSet(1, 1);
+ assertMessagesAmong0And2HaveTimerSet(1, 1);
+ }
+
+ @Test
+ public void testIntroductionAutoDeleteIntroducer() throws Exception {
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+
+ makeIntroduction(true, true);
+ assertIntroductionsArrived();
+
+ assertMessagesAmong0And1HaveTimerSet(1, 1);
+ assertMessagesAmong0And2HaveTimerSet(1, 1);
+
+ // Sync the ack to 0 from 1 - this starts 0's timer
+ ack1To0(1);
+ waitForEvents(c0);
+
+ // Before 0's timer elapses, the introducer should still see the
+ // introduction sent to 1
+ timeTravel(c0, timerLatency - 1);
+ assertGroupCountAt0With1(1, 0);
+ assertGroupCountAt0With2(1, 0);
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+
+ // After 0's timer elapses, the introducer should have deleted the
+ // introduction sent to 1
+ timeTravel(c0, 1);
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(1, 0);
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+
+ // Sync the ack to 0 from 2 - this starts 0's timer
+ ack2To0(1);
+ waitForEvents(c0);
+
+ // Before 0's timer elapses, the introducer should still see the
+ // introduction sent to 2
+ timeTravel(c0, timerLatency - 1);
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(1, 0);
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+
+ // After 0's timer elapses, the introducer should have deleted the
+ // introduction sent to 2
+ timeTravel(c0, 1);
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(0, 0);
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+ }
+
+ @Test
+ public void testIntroductionAutoDeleteIntroducee() throws Exception {
+ long timerLatency = MIN_AUTO_DELETE_TIMER_MS + BATCH_DELAY_MS;
+
+ makeIntroduction(true, false);
+
+ markMessagesRead(c1, contact0From1);
+ assertGroupCountAt1With0(1, 0);
+ assertGroupCountAt2With0(1, 1);
+
+ // Travel in time at c1 unit 1ms before the deadline expires
+ timeTravel(c1, timerLatency - 1);
+ assertGroupCountAt0With1(1, 0);
+ assertGroupCountAt0With2(1, 0);
+ assertGroupCountAt2With0(1, 1);
+ // There is currently 1 message at 1 with 0, the introduction request
+ assertGroupCountAt1With0(1, 0);
+ forEachHeader(c1, contactId0From1, 1, h ->
+ assertTrue(h instanceof IntroductionRequest)
+ );
+
+ // After travelling in time one more 1ms, the introduction should be
+ // auto-declined. We should get an event signalling that the response
+ // has been sent.
+ IntroductionResponseReceivedEvent e = assertEvent(c1,
+ IntroductionResponseReceivedEvent.class, () ->
+ timeTravel(c1, 1)
+ );
+ // the event should have correct values
+ assertEquals(contactId0From1, e.getContactId());
+ IntroductionResponse response = e.getMessageHeader();
+ assertEquals(author2.getName(),
+ response.getIntroducedAuthor().getName());
+ assertTrue(response.isAutoDecline());
+
+ // these should not have changed
+ assertGroupCountAt0With1(1, 0);
+ assertGroupCountAt0With2(1, 0);
+ assertGroupCountAt2With0(1, 1);
+ // there is still 1 message at 1 with 0, but it should now be the new
+ // auto-decline message instead of the introduction
+ assertGroupCountAt1With0(1, 0);
+ forEachHeader(c1, contactId0From1, 1, h -> {
+ assertTrue(h instanceof IntroductionResponse);
+ IntroductionResponse r = (IntroductionResponse) h;
+ assertEquals(author2.getName(), r.getIntroducedAuthor().getName());
+ assertTrue(r.isAutoDecline());
+ });
+
+ // sync auto-decline to 0
+ sync1To0(1, true);
+ waitForEvents(c0);
+ // auto-decline arrived at 0
+ assertGroupCountAt0With1(2, 1);
+
+ // forward auto-decline to 2
+ sync0To2(1, true);
+ waitForEvents(c2);
+ // auto-decline arrived at 2
+ assertGroupCountAt2With0(2, 2);
+ }
+
+ /**
+ * Let one introducee accept, the other decline manually
+ * ASSERT accept and decline messages arrive and have timer on all sides
+ * ASSERT accept and decline get forwarded to the other introducee and that
+ * they all have timers set on all sides
+ * ASSERT that all messages self-destruct
+ */
+ @Test
+ public void testIntroductionSessionManualDecline() throws Exception {
+ makeIntroduction(true, true);
+
+ assertIntroducerStatus(AWAIT_RESPONSES);
+
+ // mark messages as read on 1 and 2, this starts 1's and 2's timer for 0
+ markMessagesRead(c1, contact0From1);
+ markMessagesRead(c2, contact0From2);
+ assertGroupCountAt1With0(1, 0);
+ assertGroupCountAt2With0(1, 0);
+
+ respondToMostRecentIntroduction(c1, contactId0From1, true);
+ respondToMostRecentIntroduction(c2, contactId0From2, false);
+ sync1To0(1, true);
+ sync2To0(1, true);
+ waitForEvents(c0);
+
+ // added the own responses
+ assertGroupCountAt1With0(2, 0);
+ assertGroupCountAt2With0(2, 0);
+
+ // 0 has the sent introduction and the unread responses
+ assertGroupCountAt0With1(2, 1);
+ assertGroupCountAt0With2(2, 1);
+
+ assertIntroducerStatus(START);
+
+ markMessagesRead(c0, contact1From0);
+ markMessagesRead(c0, contact2From0);
+ assertGroupCountAt0With1(2, 0);
+ assertGroupCountAt0With2(2, 0);
+
+ // forward responses from 0 to introducees
+ sync0To1(1, true);
+ sync0To2(1, true);
+ waitForEvents(c1);
+ waitForEvents(c2);
+
+ // first contact receives the forwarded decline
+ assertGroupCountAt1With0(3, 1);
+ // second contact does not display an extra message because 2 declined
+ // themselves
+ assertGroupCountAt2With0(2, 0);
+
+ markMessagesRead(c1, contact0From1);
+ assertGroupCountAt1With0(3, 0);
+
+ assertMessagesAmong0And1HaveTimerSet(2, 3);
+ assertMessagesAmong0And2HaveTimerSet(2, 2);
+
+ // Travel in time on all devices
+ timeTravel(c0);
+ timeTravel(c1);
+ timeTravel(c2);
+
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(0, 0);
+ assertGroupCountAt1With0(0, 0);
+ assertGroupCountAt2With0(0, 0);
+ }
+
+ /**
+ * Let introductions self-destruct at the introducer and one of the
+ * introducees
+ * ASSERT that auto-declines get sent and arrive
+ * ASSERT that auto-declines have the timer set
+ * ASSERT that declines get forwarded to other introducee
+ * ASSERT that forwarded declines have the timer set
+ * ASSERT that forwarded declines self-destruct
+ * ASSERT that a that a new introduction can succeed afterwards
+ */
+ @Test
+ public void testIntroductionSessionAutoDecline() throws Exception {
+ makeIntroduction(true, false);
+
+ assertIntroducerStatus(AWAIT_RESPONSES);
+
+ // ack from 1 and 2 to 0. This starts 0's timer for 1
+ ack1To0(1);
+ ack2To0(1);
+ waitForEvents(c0);
+
+ // mark messages as read on 1 and 2, this starts 1's timer for 0
+ markMessagesRead(c1, contact0From1);
+ markMessagesRead(c2, contact0From2);
+ assertGroupCountAt1With0(1, 0);
+ assertGroupCountAt2With0(1, 0);
+
+ // Travel in time on all devices
+ timeTravel(c0);
+ timeTravel(c1);
+ timeTravel(c2);
+
+ // assert that introductions have been deleted between 0 and 1 but not
+ // between 0 and 2
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(1, 0);
+ // there is now 1 message, the auto-decline message
+ assertGroupCountAt1With0(1, 0);
+ assertGroupCountAt2With0(1, 0);
+
+ // sync the auto-decline message from 1 to 0
+ sync1To0(1, true);
+ waitForEvents(c0);
+
+ // the auto-decline from 1 has arrived at 0
+ assertGroupCountAt0With1(1, 1);
+
+ // the session status has moved to A_DECLINED or B_DECLINED
+ assertIntroducerStatusFirstDeclined();
+
+ // sync the auto-decline message from 0 to 2
+ sync0To2(1, true);
+ waitForEvents(c2);
+ // the auto-decline from 1 has arrived at 2
+ assertGroupCountAt2With0(2, 1);
+
+ // 0 and 1 still have the auto-decline message from 0
+ assertGroupCountAt0With1(1, 1);
+ assertGroupCountAt1With0(1, 0);
+
+ // make sure the auto-decline has the timer set at 0 and 1
+ assertMessagesAmong0And1HaveTimerSet(1, 1);
+
+ // ack message from 0 to 1 and make sure it self-destructs on 1
+ ack0To1(1);
+ timeTravel(c1);
+ assertGroupCountAt1With0(0, 0);
+
+ // mark message read on 0 and make sure it self-destructs on 0
+ markMessagesRead(c0, contact1From0);
+ timeTravel(c0);
+ assertGroupCountAt0With1(0, 0);
+
+ // assert that a that a new introduction can succeed afterwards:
+ // first decline from c2, assert we're in START state and then
+ // make the new introdution
+ respondToMostRecentIntroduction(c2, contactId0From2, false);
+ sync2To0(1, true);
+ waitForEvents(c0);
+ sync0To1(1, true);
+ waitForEvents(c1);
+
+ assertIntroducerStatus(START);
+ assertNewIntroductionSucceeds();
+ }
+
+ @Test
+ public void testIntroductionAcceptHasTimer() throws Exception {
+ testIntroductionResponseHasTimer(true);
+ }
+
+ @Test
+ public void testIntroductionDeclineHasTimer() throws Exception {
+ testIntroductionResponseHasTimer(false);
+ }
+
+ private void testIntroductionResponseHasTimer(boolean accept)
+ throws Exception {
+ makeIntroduction(true, false);
+ assertIntroductionsArrived();
+
+ // check that all messages have the timer set
+ assertMessagesAmong0And1HaveTimerSet(1, 1);
+ assertMessagesAmong0And2HaveTimerNotSet(1, 1);
+
+ respondToMostRecentIntroduction(c1, contactId0From1, accept);
+ sync1To0(1, true);
+ waitForEvents(c0);
+
+ // check that response has arrived
+ assertGroupCountAt0With1(2, 1);
+ assertGroupCountAt1With0(2, 1);
+ assertMessagesAmong0And1HaveTimerSet(2, 2);
+ }
+
+ @Test
+ public void testIntroductionAcceptSelfDestructs() throws Exception {
+ testIntroductionResponseSelfDestructs(true);
+ }
+
+ @Test
+ public void testIntroductionDeclineSelfDestructs() throws Exception {
+ testIntroductionResponseSelfDestructs(false);
+ }
+
+ private void testIntroductionResponseSelfDestructs(boolean accept)
+ throws Exception {
+ makeIntroduction(true, false);
+ assertIntroductionsArrived();
+
+ assertMessagesAmong0And1HaveTimerSet(1, 1);
+ assertMessagesAmong0And2HaveTimerNotSet(1, 1);
+
+ respondToMostRecentIntroduction(c1, contactId0From1, accept);
+ sync1To0(1, true);
+ waitForEvents(c0);
+
+ // Sync the ack to 1 - this starts 1's timer
+ ack0To1(1);
+ waitForEvents(c1);
+
+ // check that response has arrived
+ assertGroupCountAt0With1(2, 1);
+ assertGroupCountAt1With0(2, 1);
+ assertMessagesAmong0And1HaveTimerSet(2, 2);
+
+ // mark messages as read on 1, this starts 1's timer for 0
+ markMessagesRead(c1, contact0From1);
+ assertGroupCountAt1With0(2, 0);
+
+ // mark messages as read on 0, this starts 0's timer for 1
+ markMessagesRead(c0, contact1From0);
+ assertGroupCountAt0With1(2, 0);
+
+ // Travel in time on all devices
+ timeTravel(c0);
+ timeTravel(c1);
+ timeTravel(c2);
+
+ // assert that introductions and responses have been deleted between
+ // 0 and 1
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt1With0(0, 0);
+ }
+
+ /*
+ * Tests that checks whether an introduction can still succeed properly
+ * after the introduction self-destructed at the introducer.
+ */
+
+ /**
+ * Let introductions self-destruct at the introducer only
+ * Let both introducees accept the introduction
+ * ASSERT that accept messages still get forwarded to the other introducer
+ * ASSERT that the introduction succeeds
+ * ASSERT all messages involved self-destruct eventually
+ */
+ @Test
+ public void testSucceedAfterIntroducerSelfDestructed() throws Exception {
+ testSucceedAfterIntroducerSelfDestructed(false);
+ }
+
+ /**
+ * Variant of the above test that also auto-deletes the responses at each
+ * introducee received from the respective other introducee.
+ */
+ @Test
+ public void testSucceedAfterIntroducerAndResponsesSelfDestructed()
+ throws Exception {
+ testSucceedAfterIntroducerSelfDestructed(true);
+ }
+
+ private void testSucceedAfterIntroducerSelfDestructed(
+ boolean autoDeleteResponsesBeforeSyncingAuthAndActivate)
+ throws Exception {
+ makeIntroduction(true, true);
+
+ // ack from 1 and 2 to 0. This starts 0's timer for 1 and 2
+ ack1To0(1);
+ ack2To0(1);
+ waitForEvents(c0);
+
+ // Travel in time at 0
+ timeTravel(c0);
+
+ assertGroupCountAt0With1(0, 0);
+ assertGroupCountAt0With2(0, 0);
+
+ // -> introductions self-destructed at the introducer only
+
+ // introducees have got the introduction
+ assertGroupCountAt1With0(1, 1);
+ assertGroupCountAt2With0(1, 1);
+
+ respondToMostRecentIntroduction(c1, contactId0From1, true);
+ respondToMostRecentIntroduction(c2, contactId0From2, true);
+ sync1To0(1, true);
+ sync2To0(1, true);
+ waitForEvents(c0);
+
+ // introducees have got the own response
+ assertGroupCountAt1With0(2, 1);
+ assertGroupCountAt2With0(2, 1);
+
+ // sync forwarded ACCEPT messages to introducees
+ sync0To1(1, true);
+ sync0To2(1, true);
+ waitForEvents(c1);
+ waitForEvents(c2);
+
+ if (autoDeleteResponsesBeforeSyncingAuthAndActivate) {
+ assertGroupCountAt1With0(2, 1);
+ assertGroupCountAt2With0(2, 1);
+
+ markMessagesRead(c1, contact0From1);
+ markMessagesRead(c2, contact0From2);
+
+ assertGroupCountAt1With0(2, 0);
+ assertGroupCountAt2With0(2, 0);
+
+ // Travel in time at 1 and 2
+ timeTravel(c1);
+ timeTravel(c2);
+
+ assertGroupCountAt1With0(0, 0);
+ assertGroupCountAt2With0(0, 0);
+ }
+
+ syncAuthAndActivateMessages();
+
+ assertIntroductionSucceeded();
+ }
+
+ /*
+ * Group of three tests that check whether an introduction can still fail
+ * properly after the introduction self-destructed at the introducer.
+ *