Merge branch '2343-mailbox-as-submodule' into 'master'

End-to-end integration tests for communication via mailbox

Closes #2343

See merge request briar/briar!1699
This commit is contained in:
Torsten Grote
2022-10-03 13:11:18 +00:00
40 changed files with 791 additions and 152 deletions

View File

@@ -65,11 +65,6 @@ public class MailboxModule {
return mailboxSettingsManager;
}
@Provides
UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
return urlConverter;
}
@Provides
MailboxApi provideMailboxApi(MailboxApiImpl mailboxApi) {
return mailboxApi;

View File

@@ -9,9 +9,9 @@ import org.briarproject.nullsafety.NotNullByDefault;
@NotNullByDefault
interface UrlConverter {
/**
* Converts a raw onion address, excluding the .onion suffix, into an
* HTTP URL.
*/
String convertOnionToBaseUrl(String onion);
/**
* Converts a raw onion address, excluding the .onion suffix, into an
* HTTP URL.
*/
String convertOnionToBaseUrl(String onion);
}

View File

@@ -0,0 +1,13 @@
package org.briarproject.bramble.mailbox;
import dagger.Module;
import dagger.Provides;
@Module
public class UrlConverterModule {
@Provides
UrlConverter provideUrlConverter(UrlConverterImpl urlConverter) {
return urlConverter;
}
}

View File

@@ -1,21 +0,0 @@
#!/bin/bash
set -e
URL="http://127.0.0.1:8000/status"
attempt_counter=0
max_attempts=200 # 10min - CI for mailbox currently takes ~5min
echo "Waiting for mailbox to come online at $URL"
until [[ "$(curl -s -o /dev/null -w '%{http_code}' $URL)" == "401" ]]; do
if [ ${attempt_counter} -eq ${max_attempts} ]; then
echo "Timed out waiting for mailbox"
exit 1
fi
printf '.'
attempt_counter=$((attempt_counter + 1))
sleep 3
done
echo "Mailbox started"

View File

@@ -9,8 +9,10 @@ import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.mailbox.UrlConverterModule;
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
import org.briarproject.bramble.test.TestDnsModule;
import org.briarproject.bramble.test.TestPluginConfigModule;
import org.briarproject.bramble.test.TestSocksModule;
import java.util.concurrent.Executor;
@@ -23,8 +25,10 @@ import dagger.Component;
@Component(modules = {
BrambleCoreIntegrationTestModule.class,
BrambleCoreModule.class,
UrlConverterModule.class,
TestDnsModule.class,
TestSocksModule.class
TestSocksModule.class,
TestPluginConfigModule.class,
})
interface ContactExchangeIntegrationTestComponent
extends BrambleCoreIntegrationTestEagerSingletons {

View File

@@ -1,294 +0,0 @@
package org.briarproject.bramble.mailbox;
import org.briarproject.bramble.api.WeakSingletonProvider;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.mailbox.InvalidMailboxIdException;
import org.briarproject.bramble.api.mailbox.MailboxAuthToken;
import org.briarproject.bramble.api.mailbox.MailboxFileId;
import org.briarproject.bramble.api.mailbox.MailboxFolderId;
import org.briarproject.bramble.api.mailbox.MailboxProperties;
import org.briarproject.bramble.mailbox.MailboxApi.ApiException;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxContact;
import org.briarproject.bramble.mailbox.MailboxApi.MailboxFile;
import org.briarproject.bramble.mailbox.MailboxApi.TolerableFailureException;
import org.briarproject.bramble.test.BrambleTestCase;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;
import javax.net.SocketFactory;
import okhttp3.OkHttpClient;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
import static org.briarproject.bramble.test.TestUtils.readBytes;
import static org.briarproject.bramble.test.TestUtils.writeBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
public class MailboxIntegrationTest extends BrambleTestCase {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
private static final String URL_BASE = "http://127.0.0.1:8000";
private static final MailboxAuthToken SETUP_TOKEN;
static {
try {
SETUP_TOKEN = MailboxAuthToken.fromString(
"54686973206973206120736574757020746f6b656e20666f722042726961722e");
} catch (InvalidMailboxIdException e) {
throw new IllegalStateException();
}
}
private static final OkHttpClient client = new OkHttpClient.Builder()
.socketFactory(SocketFactory.getDefault())
.connectTimeout(60_000, MILLISECONDS)
.build();
private static final WeakSingletonProvider<OkHttpClient>
httpClientProvider =
new WeakSingletonProvider<OkHttpClient>() {
@Override
@Nonnull
public OkHttpClient createInstance() {
return client;
}
};
// We aren't using a real onion address, so use the given address verbatim
private static final UrlConverter urlConverter = onion -> onion;
private static final MailboxApiImpl api =
new MailboxApiImpl(httpClientProvider, urlConverter);
// needs to be static to keep values across different tests
private static MailboxProperties ownerProperties;
/**
* Called before each test to make sure the mailbox is setup once
* before starting with individual tests.
* {@link BeforeClass} needs to be static, so we can't use the API class.
*/
@Before
public void ensureSetup() throws IOException, ApiException {
// Skip this test unless it's explicitly enabled in the environment
assumeTrue(isOptionalTestEnabled(MailboxIntegrationTest.class));
if (ownerProperties != null) return;
MailboxProperties setupProperties = new MailboxProperties(
URL_BASE, SETUP_TOKEN, new ArrayList<>());
ownerProperties = api.setup(setupProperties);
}
@AfterClass
// we can't test wiping as a regular test as it stops the mailbox
public static void wipe() throws IOException, ApiException {
if (!isOptionalTestEnabled(MailboxIntegrationTest.class)) return;
api.wipeMailbox(ownerProperties);
// check doesn't work anymore
assertThrows(ApiException.class, () ->
api.checkStatus(ownerProperties));
// new setup doesn't work as mailbox is stopping
MailboxProperties setupProperties = new MailboxProperties(
URL_BASE, SETUP_TOKEN, new ArrayList<>());
assertThrows(ApiException.class, () -> api.setup(setupProperties));
}
@Test
public void testStatus() throws Exception {
// Owner calls status endpoint
assertTrue(api.checkStatus(ownerProperties));
// Owner adds contact
ContactId contactId = new ContactId(1);
MailboxContact contact = getMailboxContact(contactId);
MailboxProperties contactProperties = new MailboxProperties(
ownerProperties.getOnion(), contact.token,
new ArrayList<>(), contact.inboxId, contact.outboxId);
api.addContact(ownerProperties, contact);
// Contact calls status endpoint
assertTrue(api.checkStatus(contactProperties));
// Owner deletes contact again to leave clean state for other tests
api.deleteContact(ownerProperties, contactId);
assertEquals(emptyList(), api.getContacts(ownerProperties));
}
@Test
public void testContactApi() throws Exception {
ContactId contactId1 = new ContactId(1);
ContactId contactId2 = new ContactId(2);
MailboxContact mailboxContact1 = getMailboxContact(contactId1);
MailboxContact mailboxContact2 = getMailboxContact(contactId2);
// no contacts initially
assertEquals(emptyList(), api.getContacts(ownerProperties));
// added contact gets returned
api.addContact(ownerProperties, mailboxContact1);
assertEquals(singletonList(contactId1),
api.getContacts(ownerProperties));
// second contact also gets returned
api.addContact(ownerProperties, mailboxContact2);
assertEquals(Arrays.asList(contactId1, contactId2),
api.getContacts(ownerProperties));
// after both contacts get deleted, the list is empty again
api.deleteContact(ownerProperties, contactId1);
api.deleteContact(ownerProperties, contactId2);
assertEquals(emptyList(), api.getContacts(ownerProperties));
// deleting again is tolerable
assertThrows(TolerableFailureException.class,
() -> api.deleteContact(ownerProperties, contactId2));
}
@Test
public void testFileManagementApi() throws Exception {
// add contact, so we can leave each other files
ContactId contactId = new ContactId(1);
MailboxContact contact = getMailboxContact(contactId);
MailboxProperties contactProperties = new MailboxProperties(
ownerProperties.getOnion(), contact.token,
new ArrayList<>(), contact.inboxId, contact.outboxId);
api.addContact(ownerProperties, contact);
// upload a file for our contact
File file1 = folder.newFile();
byte[] bytes1 = getRandomBytes(2048);
writeBytes(file1, bytes1);
api.addFile(ownerProperties, contact.inboxId, file1);
// contact checks files
List<MailboxFile> files1 =
api.getFiles(contactProperties, contact.inboxId);
assertEquals(1, files1.size());
MailboxFileId fileName1 = files1.get(0).name;
// owner can't check files
assertThrows(TolerableFailureException.class, () ->
api.getFiles(ownerProperties, contact.inboxId));
// contact downloads file
File file1downloaded = folder.newFile();
api.getFile(contactProperties, contact.inboxId, fileName1,
file1downloaded);
assertArrayEquals(bytes1, readBytes(file1downloaded));
// owner can't download file, even if knowing name
File file1forbidden = folder.newFile();
assertThrows(TolerableFailureException.class, () ->
api.getFile(ownerProperties, contact.inboxId, fileName1,
file1forbidden));
assertEquals(0, file1forbidden.length());
// owner can't delete file
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(ownerProperties, contact.inboxId, fileName1));
// contact deletes file
api.deleteFile(contactProperties, contact.inboxId, fileName1);
assertEquals(0,
api.getFiles(contactProperties, contact.inboxId).size());
// contact uploads two files for the owner
File file2 = folder.newFile();
File file3 = folder.newFile();
byte[] bytes2 = getRandomBytes(2048);
byte[] bytes3 = getRandomBytes(1024);
writeBytes(file2, bytes2);
writeBytes(file3, bytes3);
api.addFile(contactProperties, contact.outboxId, file2);
api.addFile(contactProperties, contact.outboxId, file3);
// owner checks folders with available files
List<MailboxFolderId> folders = api.getFolders(ownerProperties);
assertEquals(singletonList(contact.outboxId), folders);
// owner lists files in contact's outbox
List<MailboxFile> files2 =
api.getFiles(ownerProperties, contact.outboxId);
assertEquals(2, files2.size());
MailboxFileId file2name = files2.get(0).name;
MailboxFileId file3name = files2.get(1).name;
// contact can't list files in contact's outbox
assertThrows(TolerableFailureException.class, () ->
api.getFiles(contactProperties, contact.outboxId));
// owner downloads both files from contact's outbox
File file2downloaded = folder.newFile();
File file3downloaded = folder.newFile();
api.getFile(ownerProperties, contact.outboxId, file2name,
file2downloaded);
api.getFile(ownerProperties, contact.outboxId, file3name,
file3downloaded);
byte[] downloadedBytes2 = readBytes(file2downloaded);
byte[] downloadedBytes3 = readBytes(file3downloaded);
// file order is preserved (sorted by time),
// so we know what file is which
assertArrayEquals(bytes2, downloadedBytes2);
assertArrayEquals(bytes3, downloadedBytes3);
// contact can't download files again, even if knowing name
File file2forbidden = folder.newFile();
File file3forbidden = folder.newFile();
assertThrows(TolerableFailureException.class, () ->
api.getFile(contactProperties, contact.outboxId, file2name,
file2forbidden));
assertThrows(TolerableFailureException.class, () ->
api.getFile(contactProperties, contact.outboxId, file3name,
file3forbidden));
assertEquals(0, file1forbidden.length());
assertEquals(0, file2forbidden.length());
// contact can't delete files in outbox
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file2name));
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(contactProperties, contact.outboxId, file3name));
// owner deletes files
api.deleteFile(ownerProperties, contact.outboxId, file2name);
api.deleteFile(ownerProperties, contact.outboxId, file3name);
assertEquals(emptyList(),
api.getFiles(ownerProperties, contact.outboxId));
assertEquals(emptyList(), api.getFolders(ownerProperties));
// deleting a non-existent file is tolerable
assertThrows(TolerableFailureException.class, () ->
api.deleteFile(ownerProperties, contact.outboxId, file3name));
// owner deletes contact again to leave clean state for other tests
api.deleteContact(ownerProperties, contactId);
assertEquals(emptyList(), api.getContacts(ownerProperties));
}
private MailboxContact getMailboxContact(ContactId contactId) {
MailboxAuthToken authToken = new MailboxAuthToken(getRandomId());
MailboxFolderId inboxId = new MailboxFolderId(getRandomId());
MailboxFolderId outboxId = new MailboxFolderId(getRandomId());
return new MailboxContact(contactId, authToken, inboxId, outboxId);
}
}

View File

@@ -9,6 +9,7 @@ import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.plugin.file.RemovableDriveManager;
import org.briarproject.bramble.battery.DefaultBatteryManagerModule;
import org.briarproject.bramble.event.DefaultEventExecutorModule;
import org.briarproject.bramble.mailbox.UrlConverterModule;
import org.briarproject.bramble.system.DefaultWakefulIoExecutorModule;
import org.briarproject.bramble.system.TimeTravelModule;
import org.briarproject.bramble.test.TestDatabaseConfigModule;
@@ -34,9 +35,10 @@ import dagger.Component;
TestMailboxDirectoryModule.class,
RemovableDriveIntegrationTestModule.class,
RemovableDriveModule.class,
UrlConverterModule.class,
TestSecureRandomModule.class,
TimeTravelModule.class,
TestSocksModule.class
TestSocksModule.class,
})
interface RemovableDriveIntegrationTestComponent
extends BrambleCoreEagerSingletons {

View File

@@ -2,8 +2,10 @@ package org.briarproject.bramble.sync;
import org.briarproject.bramble.BrambleCoreIntegrationTestEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.mailbox.UrlConverterModule;
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
import org.briarproject.bramble.test.TestDnsModule;
import org.briarproject.bramble.test.TestPluginConfigModule;
import org.briarproject.bramble.test.TestSocksModule;
import javax.inject.Singleton;
@@ -14,8 +16,10 @@ import dagger.Component;
@Component(modules = {
BrambleCoreIntegrationTestModule.class,
BrambleCoreModule.class,
UrlConverterModule.class,
TestDnsModule.class,
TestSocksModule.class
TestSocksModule.class,
TestPluginConfigModule.class,
})
interface SyncIntegrationTestComponent extends
BrambleCoreIntegrationTestEagerSingletons {

View File

@@ -14,7 +14,6 @@ import dagger.Module;
TestDatabaseConfigModule.class,
TestFeatureFlagModule.class,
TestMailboxDirectoryModule.class,
TestPluginConfigModule.class,
TestSecureRandomModule.class,
TimeTravelModule.class
})

View File

@@ -6,6 +6,7 @@ import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.connection.ConnectionManager;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.mailbox.UrlConverterModule;
import javax.inject.Singleton;
@@ -15,8 +16,10 @@ import dagger.Component;
@Component(modules = {
BrambleCoreIntegrationTestModule.class,
BrambleCoreModule.class,
UrlConverterModule.class,
TestDnsModule.class,
TestSocksModule.class
TestSocksModule.class,
TestPluginConfigModule.class,
})
public interface BrambleIntegrationTestComponent
extends BrambleCoreIntegrationTestEagerSingletons {

View File

@@ -0,0 +1,124 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.keyagreement.KeyAgreementListener;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionHandler;
import org.briarproject.bramble.api.plugin.PluginException;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties;
import org.briarproject.bramble.api.rendezvous.KeyMaterialSource;
import org.briarproject.bramble.api.rendezvous.RendezvousEndpoint;
import java.util.Collection;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.plugin.Plugin.State.ACTIVE;
import static org.briarproject.bramble.api.plugin.Plugin.State.DISABLED;
import static org.briarproject.bramble.api.plugin.Plugin.State.INACTIVE;
@NotNullByDefault
public class FakeTorPlugin implements DuplexPlugin {
private static final Logger LOG =
getLogger(FakeTorPlugin.class.getName());
private State state = INACTIVE;
@Override
public TransportId getId() {
return TorConstants.ID;
}
@Override
public long getMaxLatency() {
return 0;
}
@Override
public int getMaxIdleTime() {
return 0;
}
@Override
public void start() throws PluginException {
LOG.info("Starting plugin");
state = ACTIVE;
}
@Override
public void stop() throws PluginException {
LOG.info("Stopping plugin");
state = DISABLED;
}
@Override
public State getState() {
return state;
}
@Override
public int getReasonsDisabled() {
return 0;
}
@Override
public boolean shouldPoll() {
return false;
}
@Override
public int getPollingInterval() {
return 0;
}
@Override
public void poll(
Collection<Pair<TransportProperties, ConnectionHandler>> properties) {
// no-op
}
@Nullable
@Override
public DuplexTransportConnection createConnection(TransportProperties p) {
return null;
}
@Override
public boolean supportsKeyAgreement() {
return false;
}
@Nullable
@Override
public KeyAgreementListener createKeyAgreementListener(
byte[] localCommitment) {
return null;
}
@Nullable
@Override
public DuplexTransportConnection createKeyAgreementConnection(
byte[] remoteCommitment, BdfList descriptor) {
return null;
}
@Override
public boolean supportsRendezvous() {
return false;
}
@Nullable
@Override
public RendezvousEndpoint createRendezvousEndpoint(KeyMaterialSource k,
boolean alice, ConnectionHandler incoming) {
return null;
}
}

View File

@@ -0,0 +1,51 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginConfig;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import dagger.Module;
import dagger.Provides;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
@Module
public class FakeTorPluginConfigModule {
@Provides
PluginConfig providePluginConfig(FakeTorPluginFactory tor) {
@NotNullByDefault
PluginConfig pluginConfig = new PluginConfig() {
@Override
public Collection<DuplexPluginFactory> getDuplexFactories() {
return singletonList(tor);
}
@Override
public Collection<SimplexPluginFactory> getSimplexFactories() {
return emptyList();
}
@Override
public boolean shouldPoll() {
return false;
}
@Override
public Map<TransportId, List<TransportId>> getTransportPreferences() {
return emptyMap();
}
};
return pluginConfig;
}
}

View File

@@ -0,0 +1,37 @@
package org.briarproject.bramble.test;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.PluginCallback;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexPlugin;
import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory;
import javax.annotation.Nullable;
import javax.inject.Inject;
@NotNullByDefault
public class FakeTorPluginFactory implements DuplexPluginFactory {
private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
@Inject
FakeTorPluginFactory() {
}
@Override
public TransportId getId() {
return TorConstants.ID;
}
@Override
public long getMaxLatency() {
return MAX_LATENCY;
}
@Nullable
@Override
public DuplexPlugin createPlugin(PluginCallback callback) {
return new FakeTorPlugin();
}
}

View File

@@ -7,9 +7,11 @@ import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.properties.TransportPropertyManager;
import org.briarproject.bramble.api.transport.KeyManager;
import org.briarproject.bramble.mailbox.UrlConverterModule;
import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule;
import org.briarproject.bramble.test.BrambleIntegrationTestComponent;
import org.briarproject.bramble.test.TestDnsModule;
import org.briarproject.bramble.test.TestPluginConfigModule;
import org.briarproject.bramble.test.TestSocksModule;
import javax.inject.Singleton;
@@ -20,8 +22,10 @@ import dagger.Component;
@Component(modules = {
BrambleCoreIntegrationTestModule.class,
BrambleCoreModule.class,
UrlConverterModule.class,
TestDnsModule.class,
TestSocksModule.class
TestSocksModule.class,
TestPluginConfigModule.class,
})
interface TransportKeyAgreementTestComponent
extends BrambleIntegrationTestComponent {