mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-15 12:19:54 +01:00
Merge branch '2291-mailbox-upload-worker' into 'master'
Mailbox upload worker Closes #2291 See merge request briar/briar!1673
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFileId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
|
||||
@@ -7,6 +8,7 @@ import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
|
||||
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.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -36,6 +38,7 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
|
||||
private final MailboxFileManager mailboxFileManager =
|
||||
context.mock(MailboxFileManager.class);
|
||||
private final Cancellable apiCall = context.mock(Cancellable.class);
|
||||
|
||||
private final MailboxProperties mailboxProperties =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
@@ -96,30 +99,36 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
|
||||
// When the connectivity check succeeds, a list-inbox task should be
|
||||
// started for the first download cycle
|
||||
AtomicReference<ApiCall> listTask = new AtomicReference<>(null);
|
||||
AtomicReference<ApiCall> listTask = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(listTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the list-inbox tasks runs and finds some files to download,
|
||||
// it should start a download task for the first file
|
||||
AtomicReference<ApiCall> downloadTask = new AtomicReference<>(null);
|
||||
AtomicReference<ApiCall> downloadTask = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApi).getFiles(mailboxProperties,
|
||||
requireNonNull(mailboxProperties.getInboxId()));
|
||||
will(returnValue(files));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
assertFalse(listTask.get().callApi());
|
||||
|
||||
// When the first download task runs it should download the file to the
|
||||
// location provided by the file manager and start a delete task
|
||||
AtomicReference<ApiCall> deleteTask = new AtomicReference<>(null);
|
||||
AtomicReference<ApiCall> deleteTask = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createTempFileForDownload();
|
||||
will(returnValue(tempFile));
|
||||
@@ -128,7 +137,10 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
file1.name, tempFile);
|
||||
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
@@ -140,7 +152,10 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
requireNonNull(mailboxProperties.getInboxId()), file1.name);
|
||||
will(throwException(new TolerableFailureException()));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(downloadTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
@@ -155,7 +170,10 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
file2.name, tempFile);
|
||||
oneOf(mailboxFileManager).handleDownloadedFile(tempFile);
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(deleteTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
assertFalse(downloadTask.get().callApi());
|
||||
@@ -168,7 +186,10 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
requireNonNull(mailboxProperties.getInboxId()), file2.name);
|
||||
will(throwException(new TolerableFailureException()));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(listTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
assertFalse(deleteTask.get().callApi());
|
||||
@@ -188,7 +209,10 @@ public class ContactMailboxDownloadWorkerTest extends BrambleMockTestCase {
|
||||
// be started for the second download cycle
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new CaptureArgumentAction<>(listTask, ApiCall.class, 0));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(listTask, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onTorReachable();
|
||||
|
||||
@@ -2,16 +2,20 @@ package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager;
|
||||
import org.briarproject.bramble.api.connection.ConnectionManager.TagController;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleState;
|
||||
import org.briarproject.bramble.api.plugin.PluginManager;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionReader;
|
||||
import org.briarproject.bramble.api.plugin.TransportConnectionWriter;
|
||||
import org.briarproject.bramble.api.plugin.event.TransportActiveEvent;
|
||||
import org.briarproject.bramble.api.plugin.simplex.SimplexPlugin;
|
||||
import org.briarproject.bramble.api.properties.TransportProperties;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.ConsumeArgumentAction;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
@@ -20,6 +24,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@@ -28,11 +33,14 @@ import static org.briarproject.bramble.api.lifecycle.LifecycleManager.LifecycleS
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.ID;
|
||||
import static org.briarproject.bramble.api.plugin.file.FileConstants.PROP_PATH;
|
||||
import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.DOWNLOAD_DIR_NAME;
|
||||
import static org.briarproject.bramble.mailbox.MailboxFileManagerImpl.UPLOAD_DIR_NAME;
|
||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
|
||||
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 MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
@@ -47,6 +55,10 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
private final SimplexPlugin plugin = context.mock(SimplexPlugin.class);
|
||||
private final TransportConnectionReader transportConnectionReader =
|
||||
context.mock(TransportConnectionReader.class);
|
||||
private final TransportConnectionWriter transportConnectionWriter =
|
||||
context.mock(TransportConnectionWriter.class);
|
||||
|
||||
private final ContactId contactId = getContactId();
|
||||
|
||||
private File mailboxDir;
|
||||
private MailboxFileManagerImpl manager;
|
||||
@@ -65,17 +77,25 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
|
||||
@Test
|
||||
public void testHandlesOrphanedFilesAtStartup() throws Exception {
|
||||
// Create an orphaned file, left behind at the previous shutdown
|
||||
// Create an orphaned upload, left behind at the previous shutdown
|
||||
File uploadDir = new File(mailboxDir, UPLOAD_DIR_NAME);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
uploadDir.mkdirs();
|
||||
File orphanedUpload = new File(uploadDir, "orphan");
|
||||
assertTrue(orphanedUpload.createNewFile());
|
||||
|
||||
// Create an orphaned download, left behind at the previous shutdown
|
||||
File downloadDir = new File(mailboxDir, DOWNLOAD_DIR_NAME);
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
downloadDir.mkdirs();
|
||||
File orphan = new File(downloadDir, "orphan");
|
||||
assertTrue(orphan.createNewFile());
|
||||
File orphanedDownload = new File(downloadDir, "orphan");
|
||||
assertTrue(orphanedDownload.createNewFile());
|
||||
|
||||
TransportProperties props = new TransportProperties();
|
||||
props.put(PROP_PATH, orphan.getAbsolutePath());
|
||||
props.put(PROP_PATH, orphanedDownload.getAbsolutePath());
|
||||
|
||||
// When the plugin becomes active the orphaned file should be handled
|
||||
// When the plugin becomes active the orphaned upload should be deleted
|
||||
// and the orphaned download should be handled
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
@@ -90,10 +110,12 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
assertFalse(orphanedUpload.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenReadSucceeds() throws Exception {
|
||||
public void testDeletesDownloadedFileWhenReadSucceeds() throws Exception {
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
@@ -102,7 +124,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
new AtomicReference<>(null);
|
||||
AtomicReference<TagController> controller = new AtomicReference<>(null);
|
||||
|
||||
expectPassFileToConnectionManager(f, reader, controller);
|
||||
expectPassDownloadedFileToConnectionManager(f, reader, controller);
|
||||
manager.handleDownloadedFile(f);
|
||||
|
||||
// The read is successful, so the tag controller should allow the tag
|
||||
@@ -117,29 +139,117 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenTagIsNotRecognised() throws Exception {
|
||||
testDeletesFile(false, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletesFileWhenReadFails() throws Exception {
|
||||
testDeletesFile(true, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteFileWhenTagIsNotRecognisedAtShutdown()
|
||||
public void testDeletesDownloadedFileWhenTagIsNotRecognised()
|
||||
throws Exception {
|
||||
testDeletesFile(false, STOPPING, true);
|
||||
testDeletesDownloadedFile(false, RUNNING, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteFileWhenReadFailsAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesFile(true, STOPPING, true);
|
||||
public void testDeletesDownloadedFileWhenReadFails() throws Exception {
|
||||
testDeletesDownloadedFile(true, RUNNING, false);
|
||||
}
|
||||
|
||||
private void testDeletesFile(boolean recognised, LifecycleState state,
|
||||
boolean fileExists) throws Exception {
|
||||
@Test
|
||||
public void testDoesNotDeleteDownloadedFileWhenTagIsNotRecognisedAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesDownloadedFile(false, STOPPING, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDoesNotDeleteDownloadedFileWhenReadFailsAtShutdown()
|
||||
throws Exception {
|
||||
testDeletesDownloadedFile(true, STOPPING, true);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testThrowsExceptionIfPluginFailsToCreateWriter()
|
||||
throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(null));
|
||||
}});
|
||||
|
||||
manager.createAndWriteTempFileForUpload(contactId, sessionRecord);
|
||||
}
|
||||
|
||||
@Test(expected = IOException.class)
|
||||
public void testThrowsExceptionIfSessionFailsWithException()
|
||||
throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(transportConnectionWriter));
|
||||
oneOf(transportConnectionWriter).dispose(true);
|
||||
oneOf(connectionManager).manageOutgoingConnection(with(contactId),
|
||||
with(ID), with(any(TransportConnectionWriter.class)),
|
||||
with(sessionRecord));
|
||||
// The session fails with an exception. We need to use an action
|
||||
// for this, as createAndWriteTempFileForUpload() waits for it to
|
||||
// happen before returning
|
||||
will(new ConsumeArgumentAction<>(TransportConnectionWriter.class, 2,
|
||||
writer -> {
|
||||
try {
|
||||
writer.dispose(true);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
));
|
||||
}});
|
||||
|
||||
manager.createAndWriteTempFileForUpload(contactId, sessionRecord);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReturnsFileIfSessionSucceeds() throws Exception {
|
||||
OutgoingSessionRecord sessionRecord = new OutgoingSessionRecord();
|
||||
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(pluginManager).getPlugin(ID);
|
||||
will(returnValue(plugin));
|
||||
oneOf(plugin).createWriter(with(any(TransportProperties.class)));
|
||||
will(returnValue(transportConnectionWriter));
|
||||
oneOf(transportConnectionWriter).dispose(false);
|
||||
oneOf(connectionManager).manageOutgoingConnection(with(contactId),
|
||||
with(ID), with(any(TransportConnectionWriter.class)),
|
||||
with(sessionRecord));
|
||||
// The session succeeds. We need to use an action for this, as
|
||||
// createAndWriteTempFileForUpload() waits for it to happen before
|
||||
// returning
|
||||
will(new ConsumeArgumentAction<>(TransportConnectionWriter.class, 2,
|
||||
writer -> {
|
||||
try {
|
||||
writer.dispose(false);
|
||||
} catch (IOException e) {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
));
|
||||
}});
|
||||
|
||||
File f = manager.createAndWriteTempFileForUpload(contactId,
|
||||
sessionRecord);
|
||||
assertTrue(f.exists());
|
||||
}
|
||||
|
||||
private void testDeletesDownloadedFile(boolean recognised,
|
||||
LifecycleState state, boolean fileExists) throws Exception {
|
||||
expectCheckForOrphans();
|
||||
manager.eventOccurred(new TransportActiveEvent(ID));
|
||||
|
||||
@@ -148,7 +258,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
new AtomicReference<>(null);
|
||||
AtomicReference<TagController> controller = new AtomicReference<>(null);
|
||||
|
||||
expectPassFileToConnectionManager(f, reader, controller);
|
||||
expectPassDownloadedFileToConnectionManager(f, reader, controller);
|
||||
manager.handleDownloadedFile(f);
|
||||
|
||||
context.checking(new Expectations() {{
|
||||
@@ -169,7 +279,7 @@ public class MailboxFileManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectPassFileToConnectionManager(File f,
|
||||
private void expectPassDownloadedFileToConnectionManager(File f,
|
||||
AtomicReference<TransportConnectionReader> reader,
|
||||
AtomicReference<TagController> controller) {
|
||||
TransportProperties props = new TransportProperties();
|
||||
|
||||
@@ -13,7 +13,6 @@ import org.briarproject.bramble.api.mailbox.MailboxSettingsManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdate;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxUpdateManager;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxVersion;
|
||||
import org.briarproject.bramble.api.mailbox.event.OwnMailboxConnectionStatusEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
@@ -33,7 +32,6 @@ import static java.util.Collections.singletonList;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContact;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.hasEvent;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -138,7 +136,6 @@ public class MailboxPairingTaskImplTest extends BrambleMockTestCase {
|
||||
i.getAndIncrement();
|
||||
});
|
||||
task.run();
|
||||
hasEvent(txn, OwnMailboxConnectionStatusEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -163,7 +163,7 @@ public class MailboxSettingsManagerImplTest extends BrambleMockTestCase {
|
||||
}});
|
||||
|
||||
manager.recordSuccessfulConnection(txn, now);
|
||||
hasEvent(txn, OwnMailboxConnectionStatusEvent.class);
|
||||
assertTrue(hasEvent(txn, OwnMailboxConnectionStatusEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
package org.briarproject.bramble.mailbox;
|
||||
|
||||
import org.briarproject.bramble.api.Cancellable;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.event.EventBus;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
|
||||
import org.briarproject.bramble.api.mailbox.MailboxProperties;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.OutgoingSessionRecord;
|
||||
import org.briarproject.bramble.api.sync.event.MessageSharedEvent;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.api.system.TaskScheduler;
|
||||
import org.briarproject.bramble.test.BrambleMockTestCase;
|
||||
import org.briarproject.bramble.test.CaptureArgumentAction;
|
||||
import org.briarproject.bramble.test.ConsumeArgumentAction;
|
||||
import org.briarproject.bramble.test.DbExpectations;
|
||||
import org.briarproject.bramble.test.RunAction;
|
||||
import org.jmock.Expectations;
|
||||
import org.jmock.lib.action.DoAllAction;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.CLIENT_SUPPORTS;
|
||||
import static org.briarproject.bramble.api.mailbox.MailboxConstants.MAX_LATENCY;
|
||||
import static org.briarproject.bramble.mailbox.MailboxUploadWorker.CHECK_DELAY_MS;
|
||||
import static org.briarproject.bramble.mailbox.MailboxUploadWorker.RETRY_DELAY_MS;
|
||||
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
|
||||
import static org.briarproject.bramble.test.TestUtils.getContactId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMailboxProperties;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class MailboxUploadWorkerTest extends BrambleMockTestCase {
|
||||
|
||||
private final Executor ioExecutor = context.mock(Executor.class);
|
||||
private final DatabaseComponent db = context.mock(DatabaseComponent.class);
|
||||
private final Clock clock = context.mock(Clock.class);
|
||||
private final TaskScheduler taskScheduler =
|
||||
context.mock(TaskScheduler.class);
|
||||
private final EventBus eventBus = context.mock(EventBus.class);
|
||||
private final ConnectivityChecker connectivityChecker =
|
||||
context.mock(ConnectivityChecker.class);
|
||||
private final MailboxApiCaller mailboxApiCaller =
|
||||
context.mock(MailboxApiCaller.class);
|
||||
private final MailboxApi mailboxApi = context.mock(MailboxApi.class);
|
||||
private final MailboxFileManager mailboxFileManager =
|
||||
context.mock(MailboxFileManager.class);
|
||||
private final Cancellable apiCall =
|
||||
context.mock(Cancellable.class, "apiCall");
|
||||
private final Cancellable wakeupTask =
|
||||
context.mock(Cancellable.class, "wakeupTask");
|
||||
private final Cancellable checkTask =
|
||||
context.mock(Cancellable.class, "checkTask");
|
||||
|
||||
private final MailboxProperties mailboxProperties =
|
||||
getMailboxProperties(false, CLIENT_SUPPORTS);
|
||||
private final long now = System.currentTimeMillis();
|
||||
private final MailboxFolderId folderId = new MailboxFolderId(getRandomId());
|
||||
private final ContactId contactId = getContactId();
|
||||
private final MessageId ackedId = new MessageId(getRandomId());
|
||||
private final MessageId sentId = new MessageId(getRandomId());
|
||||
private final MessageId newMessageId = new MessageId(getRandomId());
|
||||
|
||||
private File testDir, tempFile;
|
||||
private MailboxUploadWorker worker;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testDir = getTestDirectory();
|
||||
tempFile = new File(testDir, "temp");
|
||||
worker = new MailboxUploadWorker(ioExecutor, db, clock, taskScheduler,
|
||||
eventBus, connectivityChecker, mailboxApiCaller, mailboxApi,
|
||||
mailboxFileManager, mailboxProperties, folderId, contactId);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
deleteTestDirectory(testDir);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksForDataWhenStartedAndRemovesObserverWhenDestroyed()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testChecksConnectivityWhenStartedIfDataIsReady()
|
||||
throws Exception {
|
||||
Transaction recordTxn = new Transaction(null, false);
|
||||
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// Create the temporary file so we can test that it gets deleted
|
||||
assertTrue(testDir.mkdirs());
|
||||
assertTrue(tempFile.createNewFile());
|
||||
|
||||
// When the connectivity check succeeds, the worker should write a file
|
||||
// and start an upload task
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<ApiCall> upload = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(new DoAllAction(
|
||||
// Record some IDs as acked and sent
|
||||
new ConsumeArgumentAction<>(OutgoingSessionRecord.class, 1,
|
||||
record -> {
|
||||
record.onAckSent(singletonList(ackedId));
|
||||
record.onMessageSent(sentId);
|
||||
}),
|
||||
returnValue(tempFile)
|
||||
));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(upload, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the upload task runs, it should upload the file, record
|
||||
// the acked/sent messages in the DB, and check for more data to send
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(mailboxApi).addFile(mailboxProperties, folderId, tempFile);
|
||||
oneOf(db).transaction(with(false), withDbRunnable(recordTxn));
|
||||
oneOf(db).setAckSent(recordTxn, contactId, singletonList(ackedId));
|
||||
oneOf(db).setMessagesSent(recordTxn, contactId,
|
||||
singletonList(sentId), MAX_LATENCY);
|
||||
}});
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
assertFalse(upload.get().callApi());
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// The file should have been deleted
|
||||
assertFalse(tempFile.exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsApiCallWhenDestroyed() throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// Create the temporary file so we can test that it gets deleted
|
||||
assertTrue(testDir.mkdirs());
|
||||
assertTrue(tempFile.createNewFile());
|
||||
|
||||
// When the connectivity check succeeds, the worker should write a file
|
||||
// and start an upload task
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<ApiCall> upload = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(new DoAllAction(
|
||||
// Record some IDs as acked and sent
|
||||
new ConsumeArgumentAction<>(OutgoingSessionRecord.class, 1,
|
||||
record -> {
|
||||
record.onAckSent(singletonList(ackedId));
|
||||
record.onMessageSent(sentId);
|
||||
}),
|
||||
returnValue(tempFile)
|
||||
));
|
||||
oneOf(mailboxApiCaller).retryWithBackoff(with(any(ApiCall.class)));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(upload, ApiCall.class, 0),
|
||||
returnValue(apiCall)
|
||||
));
|
||||
}});
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener and cancel the upload task
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(apiCall).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// The file should have been deleted
|
||||
assertFalse(tempFile.exists());
|
||||
|
||||
// If the upload task runs anyway (cancellation came too late), it
|
||||
// should return early when it finds the state has changed
|
||||
assertFalse(upload.get().callApi());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchedulesWakeupWhenStartedIfDataIsNotReady()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the wakeup task runs it should check for data to send
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
wakeup.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsWakeupIfDestroyedBeforeWakingUp() throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the worker is destroyed it should cancel the wakeup and
|
||||
// remove the connectivity observer and event listener
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(wakeupTask).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// If the wakeup task runs anyway (cancellation came too late), it
|
||||
// should return early without doing anything
|
||||
wakeup.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsWakeupIfEventIsReceivedBeforeWakingUp()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// the data isn't ready to send immediately, the worker should
|
||||
// schedule a wakeup
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> wakeup = new AtomicReference<>();
|
||||
expectCheckForDataToSendAndScheduleWakeup(wakeup);
|
||||
|
||||
worker.start();
|
||||
|
||||
// Before the wakeup task runs, the worker receives an event that
|
||||
// indicates new data may be available. The worker should cancel the
|
||||
// wakeup task and schedule a check for new data after a short delay
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
expectScheduleCheck(check, CHECK_DELAY_MS);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(wakeupTask).cancel();
|
||||
}});
|
||||
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// If the wakeup task runs anyway (cancellation came too late), it
|
||||
// should return early when it finds the state has changed
|
||||
wakeup.get().run();
|
||||
|
||||
// Before the check task runs, the worker receives another event that
|
||||
// indicates new data may be available. The event should be ignored,
|
||||
// as a check for new data has already been scheduled
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// When the check task runs, it should check for new data
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
check.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCancelsCheckWhenDestroyed() throws Exception {
|
||||
// When the worker is started it should check for data to send
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
worker.start();
|
||||
|
||||
// The worker receives an event that indicates new data may be
|
||||
// available. The worker should schedule a check for new data after
|
||||
// a short delay
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
expectScheduleCheck(check, CHECK_DELAY_MS);
|
||||
|
||||
worker.eventOccurred(new MessageSharedEvent(newMessageId));
|
||||
|
||||
// When the worker is destroyed it should cancel the check and
|
||||
// remove the connectivity observer and event listener
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(checkTask).cancel();
|
||||
}});
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
|
||||
// If the check runs anyway (cancellation came too late), it should
|
||||
// return early when it finds the state has changed
|
||||
check.get().run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetriesAfterDelayIfExceptionOccursWhileWritingFile()
|
||||
throws Exception {
|
||||
// When the worker is started it should check for data to send. As
|
||||
// there's data ready to send immediately, the worker should start a
|
||||
// connectivity check
|
||||
expectRunTaskOnIoExecutor();
|
||||
expectCheckForDataToSendAndStartConnectivityCheck();
|
||||
|
||||
worker.start();
|
||||
|
||||
// When the connectivity check succeeds, the worker should try to
|
||||
// write a file. This fails with an exception, so the worker should
|
||||
// retry by scheduling a check for new data after a short delay
|
||||
expectRunTaskOnIoExecutor();
|
||||
AtomicReference<Runnable> check = new AtomicReference<>();
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(mailboxFileManager).createAndWriteTempFileForUpload(
|
||||
with(contactId), with(any(OutgoingSessionRecord.class)));
|
||||
will(throwException(new IOException())); // Oh noes!
|
||||
}});
|
||||
expectScheduleCheck(check, RETRY_DELAY_MS);
|
||||
|
||||
worker.onConnectivityCheckSucceeded();
|
||||
|
||||
// When the check task runs it should check for new data
|
||||
expectCheckForDataToSendNoDataWaiting();
|
||||
|
||||
check.get().run();
|
||||
|
||||
// When the worker is destroyed it should remove the connectivity
|
||||
// observer and event listener
|
||||
expectRemoveObserverAndListener();
|
||||
|
||||
worker.destroy();
|
||||
}
|
||||
|
||||
private void expectRunTaskOnIoExecutor() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(ioExecutor).execute(with(any(Runnable.class)));
|
||||
will(new RunAction());
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendNoDataWaiting() throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(Long.MAX_VALUE)); // No data waiting
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendAndScheduleWakeup(
|
||||
AtomicReference<Runnable> wakeup) throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(now + 1234L)); // Data waiting but not ready
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(1234L), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(wakeup, Runnable.class, 0),
|
||||
returnValue(wakeupTask)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectCheckForDataToSendAndStartConnectivityCheck()
|
||||
throws Exception {
|
||||
Transaction txn = new Transaction(null, true);
|
||||
|
||||
context.checking(new DbExpectations() {{
|
||||
oneOf(db).transaction(with(true), withDbRunnable(txn));
|
||||
oneOf(db).containsAcksToSend(txn, contactId);
|
||||
will(returnValue(false));
|
||||
oneOf(db).getNextSendTime(txn, contactId, MAX_LATENCY);
|
||||
will(returnValue(0L)); // Data ready to send
|
||||
oneOf(clock).currentTimeMillis();
|
||||
will(returnValue(now));
|
||||
oneOf(connectivityChecker).checkConnectivity(mailboxProperties,
|
||||
worker);
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectScheduleCheck(AtomicReference<Runnable> check,
|
||||
long delay) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(taskScheduler).schedule(with(any(Runnable.class)),
|
||||
with(ioExecutor), with(delay), with(MILLISECONDS));
|
||||
will(new DoAllAction(
|
||||
new CaptureArgumentAction<>(check, Runnable.class, 0),
|
||||
returnValue(checkTask)
|
||||
));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRemoveObserverAndListener() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(connectivityChecker).removeObserver(worker);
|
||||
oneOf(eventBus).removeListener(worker);
|
||||
}});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.briarproject.bramble.test;
|
||||
|
||||
import org.briarproject.bramble.api.Consumer;
|
||||
import org.hamcrest.Description;
|
||||
import org.jmock.api.Action;
|
||||
import org.jmock.api.Invocation;
|
||||
|
||||
public class ConsumeArgumentAction<T> implements Action {
|
||||
|
||||
private final Class<T> capturedClass;
|
||||
private final int index;
|
||||
private final Consumer<T> consumer;
|
||||
|
||||
public ConsumeArgumentAction(Class<T> capturedClass, int index,
|
||||
Consumer<T> consumer) {
|
||||
this.capturedClass = capturedClass;
|
||||
this.index = index;
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Invocation invocation) {
|
||||
consumer.accept(capturedClass.cast(invocation.getParameter(index)));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("passes an argument to a consumer");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user