diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java new file mode 100644 index 000000000..921557027 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityChecker.java @@ -0,0 +1,32 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.ThreadSafe; + +/** + * An interface for checking whether a mailbox is reachable. + */ +@ThreadSafe +@NotNullByDefault +interface ConnectivityChecker { + + /** + * Destroys the checker. Any current connectivity check is cancelled. + */ + void destroy(); + + /** + * Starts a connectivity check if needed and calls the given observer when + * the check succeeds. If a check is already running then the observer is + * called when the check succeeds. If a connectivity check has recently + * succeeded then the observer is called immediately. + */ + void checkConnectivity(MailboxProperties properties, + ConnectivityObserver o); + + interface ConnectivityObserver { + void onConnectivityCheckSucceeded(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java new file mode 100644 index 000000000..1468c5db1 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImpl.java @@ -0,0 +1,111 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Cancellable; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.ThreadSafe; + +@ThreadSafe +@NotNullByDefault +abstract class ConnectivityCheckerImpl implements ConnectivityChecker { + + /** + * If no more than this much time has elapsed since the last connectivity + * check succeeded, consider the result to be fresh and don't check again. + *

+ * Package access for testing. + */ + static final long CONNECTIVITY_CHECK_FRESHNESS_MS = 10_000; + + private final Object lock = new Object(); + + protected final Clock clock; + private final MailboxApiCaller mailboxApiCaller; + + @GuardedBy("lock") + private boolean destroyed = false; + + @GuardedBy("lock") + @Nullable + private Cancellable connectivityCheck = null; + + @GuardedBy("lock") + private long lastConnectivityCheckSucceeded = 0; + + @GuardedBy("lock") + private final List connectivityObservers = + new ArrayList<>(); + + /** + * Creates an {@link ApiCall} for checking whether the mailbox is + * reachable. The {@link ApiCall} should call + * {@link #onConnectivityCheckSucceeded(long)} if the check succeeds. + */ + abstract ApiCall createConnectivityCheckTask(MailboxProperties properties); + + ConnectivityCheckerImpl(Clock clock, MailboxApiCaller mailboxApiCaller) { + this.clock = clock; + this.mailboxApiCaller = mailboxApiCaller; + } + + @Override + public void destroy() { + synchronized (lock) { + destroyed = true; + connectivityObservers.clear(); + if (connectivityCheck != null) { + connectivityCheck.cancel(); + connectivityCheck = null; + } + } + } + + @Override + public void checkConnectivity(MailboxProperties properties, + ConnectivityObserver o) { + boolean callNow = false; + synchronized (lock) { + if (destroyed) return; + if (connectivityCheck == null) { + // No connectivity check is running + long now = clock.currentTimeMillis(); + if (now - lastConnectivityCheckSucceeded + > CONNECTIVITY_CHECK_FRESHNESS_MS) { + // The last connectivity check is stale, start a new one + connectivityObservers.add(o); + ApiCall task = + createConnectivityCheckTask(properties); + connectivityCheck = mailboxApiCaller.retryWithBackoff(task); + } else { + // The last connectivity check is fresh + callNow = true; + } + } else { + // A connectivity check is running, wait for it to succeed + connectivityObservers.add(o); + } + } + if (callNow) o.onConnectivityCheckSucceeded(); + } + + protected void onConnectivityCheckSucceeded(long now) { + List observers; + synchronized (lock) { + if (destroyed) return; + connectivityCheck = null; + lastConnectivityCheckSucceeded = now; + observers = new ArrayList<>(connectivityObservers); + connectivityObservers.clear(); + } + for (ConnectivityObserver o : observers) { + o.onConnectivityCheckSucceeded(); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java new file mode 100644 index 000000000..03ca2e257 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityChecker.java @@ -0,0 +1,40 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.MailboxApi.ApiException; + +import java.io.IOException; + +import javax.annotation.concurrent.ThreadSafe; + +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; + +@ThreadSafe +@NotNullByDefault +class ContactMailboxConnectivityChecker extends ConnectivityCheckerImpl { + + private final MailboxApi mailboxApi; + + ContactMailboxConnectivityChecker(Clock clock, + MailboxApiCaller mailboxApiCaller, MailboxApi mailboxApi) { + super(clock, mailboxApiCaller); + this.mailboxApi = mailboxApi; + } + + @Override + ApiCall createConnectivityCheckTask(MailboxProperties properties) { + if (properties.isOwner()) throw new IllegalArgumentException(); + return new SimpleApiCall() { + @Override + void tryToCallApi() throws IOException, ApiException { + mailboxApi.getFiles(properties, + requireNonNull(properties.getInboxId())); + // Call the observers and cache the result + onConnectivityCheckSucceeded(clock.currentTimeMillis()); + } + }; + } + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java new file mode 100644 index 000000000..af6b12264 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityChecker.java @@ -0,0 +1,70 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.MailboxApi.ApiException; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.annotation.concurrent.ThreadSafe; + +import static java.util.logging.Level.WARNING; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.util.LogUtils.logException; + +@ThreadSafe +@NotNullByDefault +class OwnMailboxConnectivityChecker extends ConnectivityCheckerImpl { + + private static final Logger LOG = + getLogger(OwnMailboxConnectivityChecker.class.getName()); + + private final MailboxApi mailboxApi; + private final TransactionManager db; + private final MailboxSettingsManager mailboxSettingsManager; + + OwnMailboxConnectivityChecker(Clock clock, + MailboxApiCaller mailboxApiCaller, + MailboxApi mailboxApi, + TransactionManager db, + MailboxSettingsManager mailboxSettingsManager) { + super(clock, mailboxApiCaller); + this.mailboxApi = mailboxApi; + this.db = db; + this.mailboxSettingsManager = mailboxSettingsManager; + } + + @Override + ApiCall createConnectivityCheckTask(MailboxProperties properties) { + if (!properties.isOwner()) throw new IllegalArgumentException(); + return () -> { + try { + try { + mailboxApi.getFolders(properties); + LOG.info("Own mailbox is reachable"); + long now = clock.currentTimeMillis(); + db.transaction(false, txn -> mailboxSettingsManager + .recordSuccessfulConnection(txn, now)); + // Call the observers and cache the result + onConnectivityCheckSucceeded(now); + return false; // Don't retry + } catch (IOException | ApiException e) { + LOG.warning("Own mailbox is unreachable"); + logException(LOG, WARNING, e); + long now = clock.currentTimeMillis(); + db.transaction(false, txn -> mailboxSettingsManager + .recordFailedConnectionAttempt(txn, now)); + } + } catch (DbException e) { + logException(LOG, WARNING, e); + } + return true; // Retry + }; + } + +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java new file mode 100644 index 000000000..448b36ea9 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ConnectivityCheckerImplTest.java @@ -0,0 +1,201 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Cancellable; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.jmock.Expectations; +import org.junit.Test; + +import javax.annotation.Nonnull; + +import static org.briarproject.bramble.mailbox.ConnectivityCheckerImpl.CONNECTIVITY_CHECK_FRESHNESS_MS; +import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS; +import static org.briarproject.bramble.test.TestUtils.getMailboxProperties; + +public class ConnectivityCheckerImplTest extends BrambleMockTestCase { + + private final Clock clock = context.mock(Clock.class); + private final MailboxApiCaller mailboxApiCaller = + context.mock(MailboxApiCaller.class); + private final ApiCall apiCall = context.mock(ApiCall.class); + private final Cancellable task = context.mock(Cancellable.class); + private final ConnectivityObserver observer1 = + context.mock(ConnectivityObserver.class, "1"); + private final ConnectivityObserver observer2 = + context.mock(ConnectivityObserver.class, "2"); + + private final MailboxProperties properties = + getMailboxProperties(true, CLIENT_SUPPORTS); + private final long now = System.currentTimeMillis(); + + @Test + public void testFirstObserverStartsCheck() { + ConnectivityCheckerImpl checker = createChecker(); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer1); + + // When the check succeeds the observer should be called + context.checking(new Expectations() {{ + oneOf(observer1).onConnectivityCheckSucceeded(); + }}); + + checker.onConnectivityCheckSucceeded(now); + + // The observer should not be called again when subsequent checks + // succeed + checker.onConnectivityCheckSucceeded(now); + } + + @Test + public void testObserverIsAddedToExistingCheck() { + ConnectivityCheckerImpl checker = createChecker(); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer1); + + // When checkConnectivity() is called again before the first check + // succeeds, the observer should be added to the existing check + checker.checkConnectivity(properties, observer2); + + // When the check succeeds both observers should be called + context.checking(new Expectations() {{ + oneOf(observer1).onConnectivityCheckSucceeded(); + oneOf(observer2).onConnectivityCheckSucceeded(); + }}); + + checker.onConnectivityCheckSucceeded(now); + + // The observers should not be called again when subsequent checks + // succeed + checker.onConnectivityCheckSucceeded(now); + } + + @Test + public void testFreshResultIsReused() { + ConnectivityCheckerImpl checker = createChecker(); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer1); + + // When the check succeeds the observer should be called + context.checking(new Expectations() {{ + oneOf(observer1).onConnectivityCheckSucceeded(); + }}); + + checker.onConnectivityCheckSucceeded(now); + + // When checkConnectivity() is called again within + // CONNECTIVITY_CHECK_FRESHNESS_MS the observer should be called with + // the result of the recent check + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS)); + oneOf(observer2).onConnectivityCheckSucceeded(); + }}); + + checker.checkConnectivity(properties, observer2); + } + + @Test + public void testStaleResultIsNotReused() { + ConnectivityCheckerImpl checker = createChecker(); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer1); + + // When the check succeeds the observer should be called + context.checking(new Expectations() {{ + oneOf(observer1).onConnectivityCheckSucceeded(); + }}); + + checker.onConnectivityCheckSucceeded(now); + + // When checkConnectivity() is called again after more than + // CONNECTIVITY_CHECK_FRESHNESS_MS another check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer2); + + // When the check succeeds the observer should be called + context.checking(new Expectations() {{ + oneOf(observer2).onConnectivityCheckSucceeded(); + }}); + + checker.onConnectivityCheckSucceeded( + now + CONNECTIVITY_CHECK_FRESHNESS_MS + 1); + } + + @Test + public void testCheckIsCancelledWhenCheckerIsDestroyed() { + ConnectivityCheckerImpl checker = createChecker(); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(apiCall); + will(returnValue(task)); + }}); + + checker.checkConnectivity(properties, observer1); + + // When the checker is destroyed the check should be cancelled + context.checking(new Expectations() {{ + oneOf(task).cancel(); + }}); + + checker.destroy(); + + // If the check runs anyway (cancellation came too late) the observer + // should not be called + checker.onConnectivityCheckSucceeded(now); + } + + private ConnectivityCheckerImpl createChecker() { + + return new ConnectivityCheckerImpl(clock, mailboxApiCaller) { + @Override + @Nonnull + protected ApiCall createConnectivityCheckTask( + @Nonnull MailboxProperties properties) { + return apiCall; + } + }; + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java new file mode 100644 index 000000000..222226c00 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/ContactMailboxConnectivityCheckerTest.java @@ -0,0 +1,102 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Cancellable; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; +import org.jmock.Expectations; +import org.jmock.lib.action.DoAllAction; +import org.junit.Test; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Collections.emptyList; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS; +import static org.briarproject.bramble.test.TestUtils.getMailboxProperties; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ContactMailboxConnectivityCheckerTest extends BrambleMockTestCase { + + private final Clock clock = context.mock(Clock.class); + private final MailboxApiCaller mailboxApiCaller = + context.mock(MailboxApiCaller.class); + private final MailboxApi mailboxApi = context.mock(MailboxApi.class); + private final Cancellable task = context.mock(Cancellable.class); + private final ConnectivityObserver observer = + context.mock(ConnectivityObserver.class); + + private final MailboxProperties properties = + getMailboxProperties(false, CLIENT_SUPPORTS); + private final long now = System.currentTimeMillis(); + + @Test + public void testObserverIsCalledWhenCheckSucceeds() throws Exception { + ContactMailboxConnectivityChecker checker = createChecker(); + AtomicReference apiCall = new AtomicReference<>(null); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class))); + will(new DoAllAction( + new CaptureArgumentAction<>(apiCall, ApiCall.class, 0), + returnValue(task) + )); + }}); + + checker.checkConnectivity(properties, observer); + + // When the check succeeds the observer should be called + context.checking(new Expectations() {{ + oneOf(mailboxApi).getFiles(properties, + requireNonNull(properties.getInboxId())); + will(returnValue(emptyList())); + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(observer).onConnectivityCheckSucceeded(); + }}); + + // The call should not be retried + assertFalse(apiCall.get().callApi()); + } + + @Test + public void testObserverIsNotCalledWhenCheckFails() throws Exception { + ContactMailboxConnectivityChecker checker = createChecker(); + AtomicReference apiCall = new AtomicReference<>(null); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class))); + will(new DoAllAction( + new CaptureArgumentAction<>(apiCall, ApiCall.class, 0), + returnValue(task) + )); + }}); + + checker.checkConnectivity(properties, observer); + + // When the check fails, the observer should not be called + context.checking(new Expectations() {{ + oneOf(mailboxApi).getFiles(properties, + requireNonNull(properties.getInboxId())); + will(throwException(new IOException())); + }}); + + // The call should be retried + assertTrue(apiCall.get().callApi()); + } + + private ContactMailboxConnectivityChecker createChecker() { + return new ContactMailboxConnectivityChecker(clock, mailboxApiCaller, + mailboxApi); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java new file mode 100644 index 000000000..2f4678b25 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/mailbox/OwnMailboxConnectivityCheckerTest.java @@ -0,0 +1,118 @@ +package org.briarproject.bramble.mailbox; + +import org.briarproject.bramble.api.Cancellable; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.mailbox.MailboxProperties; +import org.briarproject.bramble.api.mailbox.MailboxSettingsManager; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.mailbox.ConnectivityChecker.ConnectivityObserver; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; +import org.briarproject.bramble.test.DbExpectations; +import org.jmock.Expectations; +import org.jmock.lib.action.DoAllAction; +import org.junit.Test; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.Collections.emptyList; +import static org.briarproject.bramble.mailbox.MailboxApi.CLIENT_SUPPORTS; +import static org.briarproject.bramble.test.TestUtils.getMailboxProperties; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OwnMailboxConnectivityCheckerTest extends BrambleMockTestCase { + + private final Clock clock = context.mock(Clock.class); + private final MailboxApiCaller mailboxApiCaller = + context.mock(MailboxApiCaller.class); + private final MailboxApi mailboxApi = context.mock(MailboxApi.class); + private final TransactionManager db = + context.mock(TransactionManager.class); + private final MailboxSettingsManager mailboxSettingsManager = + context.mock(MailboxSettingsManager.class); + private final Cancellable task = context.mock(Cancellable.class); + private final ConnectivityObserver observer = + context.mock(ConnectivityObserver.class); + + private final MailboxProperties properties = + getMailboxProperties(true, CLIENT_SUPPORTS); + private final long now = System.currentTimeMillis(); + + @Test + public void testObserverIsCalledWhenCheckSucceeds() throws Exception { + OwnMailboxConnectivityChecker checker = createChecker(); + AtomicReference apiCall = new AtomicReference<>(null); + Transaction txn = new Transaction(null, false); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class))); + will(new DoAllAction( + new CaptureArgumentAction<>(apiCall, ApiCall.class, 0), + returnValue(task) + )); + }}); + + checker.checkConnectivity(properties, observer); + + // When the check succeeds, the success should be recorded in the DB + // and the observer should be called + context.checking(new DbExpectations() {{ + oneOf(mailboxApi).getFolders(properties); + will(returnValue(emptyList())); + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(db).transaction(with(false), withDbRunnable(txn)); + oneOf(mailboxSettingsManager).recordSuccessfulConnection(txn, now); + oneOf(observer).onConnectivityCheckSucceeded(); + }}); + + // The call should not be retried + assertFalse(apiCall.get().callApi()); + } + + @Test + public void testObserverIsNotCalledWhenCheckFails() throws Exception { + OwnMailboxConnectivityChecker checker = createChecker(); + AtomicReference apiCall = new AtomicReference<>(null); + Transaction txn = new Transaction(null, false); + + // When checkConnectivity() is called a check should be started + context.checking(new Expectations() {{ + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class))); + will(new DoAllAction( + new CaptureArgumentAction<>(apiCall, ApiCall.class, 0), + returnValue(task) + )); + }}); + + checker.checkConnectivity(properties, observer); + + // When the check fails, the failure should be recorded in the DB and + // the observer should not be called + context.checking(new DbExpectations() {{ + oneOf(mailboxApi).getFolders(properties); + will(throwException(new IOException())); + oneOf(clock).currentTimeMillis(); + will(returnValue(now)); + oneOf(db).transaction(with(false), withDbRunnable(txn)); + oneOf(mailboxSettingsManager) + .recordFailedConnectionAttempt(txn, now); + }}); + + // The call should be retried + assertTrue(apiCall.get().callApi()); + } + + private OwnMailboxConnectivityChecker createChecker() { + return new OwnMailboxConnectivityChecker(clock, mailboxApiCaller, + mailboxApi, db, mailboxSettingsManager); + } +}