diff --git a/.run/All unit tests.run.xml b/.run/All unit tests.run.xml index 95c8e8b28220babaccac81bddf1f6bd1614e6994..ebe148c5b1905281395d2c132cf120d9c339acce 100644 --- a/.run/All unit tests.run.xml +++ b/.run/All unit tests.run.xml @@ -1,9 +1,24 @@ <component name="ProjectRunConfigurationManager"> - <configuration default="false" name="All unit tests" type="CompoundRunConfigurationType"> - <toRun name="mailbox [ktlintCheck]" type="GradleRunConfiguration" /> - <toRun name=":mailbox-android:test" type="GradleRunConfiguration" /> - <toRun name=":mailbox-core:test" type="GradleRunConfiguration" /> - <toRun name=":mailbox-cli:test" type="GradleRunConfiguration" /> + <configuration default="false" name="All unit tests" type="GradleRunConfiguration" + factoryName="Gradle"> + <ExternalSystemSettings> + <option name="executionName" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="externalSystemIdString" value="GRADLE" /> + <option name="scriptParameters" value="" /> + <option name="taskDescriptions"> + <list /> + </option> + <option name="taskNames"> + <list> + <option value="check" /> + </list> + </option> + <option name="vmOptions" value="" /> + </ExternalSystemSettings> + <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess> + <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess> + <DebugAllEnabled>false</DebugAllEnabled> <method v="2" /> </configuration> </component> \ No newline at end of file diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java index 03567871b02ea5362a2947bdf5ffd55f95e527f2..27654404856566c46e62b7e2322f3ff60a982019 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManager.java @@ -1,6 +1,7 @@ package org.briarproject.mailbox.core.lifecycle; import org.briarproject.mailbox.core.db.Database; +import org.briarproject.mailbox.core.db.Transaction; import org.briarproject.mailbox.core.system.Wakeful; import java.util.concurrent.ExecutorService; @@ -107,6 +108,6 @@ public interface LifecycleManager { * {@link #waitForDatabase()} returns. */ @Wakeful - void onDatabaseOpened(); + void onDatabaseOpened(Transaction txn); } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt index 1ab9b3549ed4d6196c59bee7807a8f02c49d6f72..0435916693ded6383645c9c6ae7554f9ad8e52c3 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.kt @@ -80,6 +80,12 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database val reopened = db.open(this) if (reopened) logDuration(LOG, { "Reopening database" }, start) else logDuration(LOG, { "Creating database" }, start) + // Inform hooks that DB was opened + db.write { txn -> + for (hook in openDatabaseHooks) { + hook.onDatabaseOpened(txn) + } + } LOG.info("Starting services") state.value = STARTING_SERVICES dbLatch.countDown() diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt index 0aaf277b5c9bbe07ca044abfd33c1f842ba585b1..16baaf81c145f8115381e173b9dbd41f38665b41 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/AuthManager.kt @@ -6,6 +6,7 @@ import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.server.MailboxPrincipal.ContactPrincipal import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal +import org.briarproject.mailbox.core.settings.MetadataManager import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.RandomIdManager import javax.inject.Inject @@ -15,6 +16,7 @@ import javax.inject.Singleton class AuthManager @Inject constructor( private val db: Database, private val setupManager: SetupManager, + private val metadataManager: MetadataManager, private val randomIdManager: RandomIdManager, ) { @@ -24,7 +26,7 @@ class AuthManager @Inject constructor( */ fun getPrincipal(token: String): MailboxPrincipal? { randomIdManager.assertIsRandomId(token) - return db.read { txn -> + val principal = db.read { txn -> val contact = db.getContactWithToken(txn, token) when { contact != null -> ContactPrincipal(contact) @@ -33,6 +35,10 @@ class AuthManager @Inject constructor( else -> null } } + // We register the owner connection here before further call validation. + // It can still happen that the owner sends invalid requests, but that's fine here. + if (principal is OwnerPrincipal) metadataManager.onOwnerConnected() + return principal } /** @@ -77,6 +83,14 @@ class AuthManager @Inject constructor( if (principal !is OwnerPrincipal) throw AuthException() } + /** + * @throws [AuthException] when given [principal] is NOT the setup token. + */ + @Throws(AuthException::class) + fun assertIsSetup(principal: MailboxPrincipal?) { + if (principal !is SetupPrincipal) throw AuthException() + } + } sealed class MailboxPrincipal : Principal { diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt index 20ecdee08460628233187c39fc9244cb35b8f83d..aa83f0172ae07e6d95200a98135538347f0a4cf8 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/Routing.kt @@ -4,7 +4,6 @@ import io.ktor.application.Application import io.ktor.application.ApplicationCall import io.ktor.application.call import io.ktor.auth.authenticate -import io.ktor.auth.principal import io.ktor.features.BadRequestException import io.ktor.features.MissingRequestParameterException import io.ktor.http.ContentType @@ -22,14 +21,16 @@ import io.ktor.routing.routing import io.ktor.util.getOrFail import org.briarproject.mailbox.core.contacts.ContactsManager import org.briarproject.mailbox.core.files.FileManager -import org.briarproject.mailbox.core.setup.SetupManager +import org.briarproject.mailbox.core.settings.MetadataRouteManager +import org.briarproject.mailbox.core.setup.SetupRouteManager import org.briarproject.mailbox.core.setup.WipeManager import org.briarproject.mailbox.core.system.InvalidIdException internal const val V = "/" // TODO set to "/v1" for release internal fun Application.configureBasicApi( - setupManager: SetupManager, + metadataRouteManager: MetadataRouteManager, + setupRouteManager: SetupRouteManager, wipeManager: WipeManager, ) = routing { route(V) { @@ -43,9 +44,7 @@ internal fun Application.configureBasicApi( authenticate { get("/status") { call.handle { - if (call.principal<MailboxPrincipal>() !is MailboxPrincipal.OwnerPrincipal) - throw AuthException() - call.respond(HttpStatusCode.OK) + metadataRouteManager.onStatusRequest(call) } } delete { @@ -55,7 +54,7 @@ internal fun Application.configureBasicApi( } put("/setup") { call.handle { - setupManager.onSetupRequest(call) + setupRouteManager.onSetupRequest(call) } } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt index 96889cbbb6a9871806910feeffa4602beb9cdf6e..34edb4591bf784a3d54482536bd1fbe2ff88db4b 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt @@ -11,7 +11,8 @@ import org.briarproject.mailbox.core.contacts.ContactsManager import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.lifecycle.Service import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT -import org.briarproject.mailbox.core.setup.SetupManager +import org.briarproject.mailbox.core.settings.MetadataRouteManager +import org.briarproject.mailbox.core.setup.SetupRouteManager import org.briarproject.mailbox.core.setup.WipeManager import org.slf4j.Logger import org.slf4j.LoggerFactory.getLogger @@ -27,7 +28,8 @@ interface WebServerManager : Service { @Singleton internal class WebServerManagerImpl @Inject constructor( private val authManager: AuthManager, - private val setupManager: SetupManager, + private val metadataRouteManager: MetadataRouteManager, + private val setupRouteManager: SetupRouteManager, private val contactsManager: ContactsManager, private val fileManager: FileManager, private val wipeManager: WipeManager, @@ -52,7 +54,7 @@ internal class WebServerManagerImpl @Inject constructor( install(ContentNegotiation) { jackson() } - configureBasicApi(setupManager, wipeManager) + configureBasicApi(metadataRouteManager, setupRouteManager, wipeManager) configureContactApi(contactsManager) configureFilesApi(fileManager) } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/MetadataManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/MetadataManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..b24da7ed4fd769a0478136b2ba74235e03346686 --- /dev/null +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/MetadataManager.kt @@ -0,0 +1,73 @@ +package org.briarproject.mailbox.core.settings + +import io.ktor.application.ApplicationCall +import io.ktor.auth.principal +import io.ktor.http.HttpStatusCode +import io.ktor.response.respond +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import org.briarproject.mailbox.core.db.DbException +import org.briarproject.mailbox.core.db.Transaction +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook +import org.briarproject.mailbox.core.server.AuthException +import org.briarproject.mailbox.core.server.AuthManager +import javax.inject.Inject + +interface MetadataManager : OpenDatabaseHook { + + /** + * Call this after the owner authenticated. + * It stores the current timestamp in settings. + */ + @Throws(DbException::class) + fun onOwnerConnected() + + /** + * The epoch timestamp in milliseconds when we saw the last owner connection. + * Attention: Only access after [LifecycleManager] has finished starting. + * Will be `0` before or when owner has never connected. + */ + val ownerConnectionTime: StateFlow<Long> + +} + +private const val SETTINGS_NAMESPACE_OWNER_METADATA = "ownerMetadata" +private const val SETTINGS_LAST_CONNECTION_TIME = "lastConnectionTime" + +class MetadataManagerImpl @Inject constructor( + private val settingsManager: SettingsManager, +) : MetadataManager { + + private val _ownerConnectionTime = MutableStateFlow(0L) + override val ownerConnectionTime: StateFlow<Long> = _ownerConnectionTime + + override fun onDatabaseOpened(txn: Transaction) { + val s = settingsManager.getSettings(txn, SETTINGS_NAMESPACE_OWNER_METADATA) + _ownerConnectionTime.value = s.getLong(SETTINGS_LAST_CONNECTION_TIME, 0L) + } + + @Throws(DbException::class) + override fun onOwnerConnected() { + val timestamp = System.currentTimeMillis() + val s = Settings().apply { + putLong(SETTINGS_LAST_CONNECTION_TIME, timestamp) + } + settingsManager.mergeSettings(s, SETTINGS_NAMESPACE_OWNER_METADATA) + _ownerConnectionTime.value = timestamp + } + +} + +class MetadataRouteManager @Inject constructor( + private val authManager: AuthManager, +) { + /** + * GET /status + */ + @Throws(AuthException::class) + suspend fun onStatusRequest(call: ApplicationCall) { + authManager.assertIsOwner(call.principal()) + call.respond(HttpStatusCode.OK) + } +} diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt index 59a199d2f3eb50c02ec1edc9614eb6154cbc54ee..830e05f05a0d10646100c5b3be67a3dc0bf1cdc4 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsManagerImpl.kt @@ -4,10 +4,9 @@ import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.db.DbException import org.briarproject.mailbox.core.db.Transaction import javax.annotation.concurrent.Immutable -import javax.inject.Inject @Immutable -internal class SettingsManagerImpl @Inject constructor(private val db: Database) : SettingsManager { +internal class SettingsManagerImpl(private val db: Database) : SettingsManager { @Throws(DbException::class) override fun getSettings(namespace: String): Settings { diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt index 75490e09ff388f8b27c6bd5873d244c7cdbea4fd..e936c6d823455c5c709794a0a4f824be531979d5 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/settings/SettingsModule.kt @@ -3,11 +3,25 @@ package org.briarproject.mailbox.core.settings import dagger.Module import dagger.Provides import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import javax.inject.Singleton @Module class SettingsModule { @Provides + @Singleton fun provideSettingsManager(db: Database): SettingsManager { return SettingsManagerImpl(db) } + + @Provides + @Singleton + fun provideMetadataManager( + metadataManagerImpl: MetadataManagerImpl, + lifecycleManager: LifecycleManager, + ): MetadataManager { + return metadataManagerImpl.also { + lifecycleManager.registerOpenDatabaseHook(it) + } + } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt index eb07a5ed5186baf822b8bad58cff56273520a6df..052ec7880077eff1fa9e809ae0fe9fbb9447c139 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/SetupManager.kt @@ -7,8 +7,7 @@ import io.ktor.response.respond import org.briarproject.mailbox.core.db.DbException import org.briarproject.mailbox.core.db.Transaction import org.briarproject.mailbox.core.server.AuthException -import org.briarproject.mailbox.core.server.MailboxPrincipal -import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal +import org.briarproject.mailbox.core.server.AuthManager import org.briarproject.mailbox.core.settings.Settings import org.briarproject.mailbox.core.settings.SettingsManager import org.briarproject.mailbox.core.system.RandomIdManager @@ -33,24 +32,6 @@ class SetupManager @Inject constructor( settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE_OWNER) } - /** - * Handler for `PUT /setup` API endpoint. - * - * Wipes setup token and responds with new owner token and 201 status code. - */ - @Throws(AuthException::class) - suspend fun onSetupRequest(call: ApplicationCall) { - val principal: MailboxPrincipal? = call.principal() - if (principal !is SetupPrincipal) throw AuthException() - - // set new owner token and clear single-use setup token - val ownerToken = randomIdManager.getNewRandomId() - setToken(null, ownerToken) - val response = SetupResponse(ownerToken) - - call.respond(HttpStatusCode.Created, response) - } - /** * Visible for testing, consider private. */ @@ -77,4 +58,27 @@ class SetupManager @Inject constructor( } +class SetupRouteManager @Inject constructor( + private val authManager: AuthManager, + private val setupManager: SetupManager, + private val randomIdManager: RandomIdManager, +) { + /** + * Handler for `PUT /setup` API endpoint. + * + * Wipes setup token and responds with new owner token and 201 status code. + */ + @Throws(AuthException::class) + suspend fun onSetupRequest(call: ApplicationCall) { + authManager.assertIsSetup(call.principal()) + + // set new owner token and clear single-use setup token + val ownerToken = randomIdManager.getNewRandomId() + setupManager.setToken(null, ownerToken) + val response = SetupResponse(ownerToken) + + call.respond(HttpStatusCode.Created, response) + } +} + internal data class SetupResponse(val token: String) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt index 6eeff9ca8f6c74f13c17479c5f3e0fc58c656c81..539a39dd52a7eb10a632275c5145fc22fc01ef66 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestComponent.kt @@ -5,6 +5,7 @@ import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.files.FileProvider import org.briarproject.mailbox.core.lifecycle.LifecycleManager +import org.briarproject.mailbox.core.settings.MetadataManager import org.briarproject.mailbox.core.settings.SettingsManager import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.RandomIdManager @@ -25,4 +26,5 @@ interface TestComponent { fun getDatabase(): Database fun getRandomIdManager(): RandomIdManager fun getFileProvider(): FileProvider + fun getMetadataManager(): MetadataManager } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt index 90b367b0fc48784c38397a24d15bb0b30b2adc8b..716de09192d73f785cc575c5e23229d8e61e0559 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestUtils.kt @@ -15,6 +15,8 @@ import org.briarproject.mailbox.core.util.IoUtils import java.io.File import kotlin.random.Random import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue object TestUtils { @@ -79,4 +81,12 @@ object TestUtils { assertEquals(expected, actual) } + fun assertTimestampRecent(timestamp: Long) { + assertNotEquals(0, timestamp) + assertTrue( + System.currentTimeMillis() - timestamp < 1000, + "Timestamp is ${System.currentTimeMillis() - timestamp}ms old." + ) + } + } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt index bb11417d6cd8180373c0f2acbf8cf2e3410a4e54..cc606b857fed7af0c3e6777b409b02cdd4ebf370 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerIntegrationTest.kt @@ -15,8 +15,8 @@ import io.ktor.http.HttpStatusCode.Companion.Unauthorized import io.ktor.http.contentType import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.TestUtils.assertJson +import org.briarproject.mailbox.core.TestUtils.assertTimestampRecent import org.briarproject.mailbox.core.TestUtils.getNewRandomContact -import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.server.IntegrationTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -25,9 +25,6 @@ import kotlin.test.assertEquals class ContactsManagerIntegrationTest : IntegrationTest() { - val db: Database - get() = testComponent.getDatabase() - @BeforeEach fun initDb() { addOwnerToken() @@ -37,15 +34,20 @@ class ContactsManagerIntegrationTest : IntegrationTest() { fun clearDb() { db.write { txn -> db.clearDatabase(txn) + // clears [metadataManager.ownerConnectionTime] + metadataManager.onDatabaseOpened(txn) } } @Test fun `get contacts is initially empty`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(ownerToken) } assertJson("""{ "contacts": [ ] }""", response) + + assertTimestampRecent(metadataManager.ownerConnectionTime.value) } @Test @@ -60,12 +62,14 @@ class ContactsManagerIntegrationTest : IntegrationTest() { @Test fun `get contacts rejects unauthorized for contacts`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) addContact(contact1) addContact(contact2) val response: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(contact1.token) } assertEquals(Unauthorized, response.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) } @Test @@ -108,6 +112,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() { @Test fun `owner can add contacts`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val c1 = getNewRandomContact(1).also { addContact(it) } val c2 = getNewRandomContact(2).also { addContact(it) } val c3 = getNewRandomContact(3) @@ -119,6 +124,8 @@ class ContactsManagerIntegrationTest : IntegrationTest() { } assertEquals(Created, response1.status) + assertTimestampRecent(metadataManager.ownerConnectionTime.value) + val response2: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(ownerToken) } @@ -154,6 +161,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() { @Test fun `owner can remove contacts`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) addContact(getNewRandomContact(1)) addContact(getNewRandomContact(2)) @@ -162,6 +170,8 @@ class ContactsManagerIntegrationTest : IntegrationTest() { } assertEquals(OK, response1.status) + assertTimestampRecent(metadataManager.ownerConnectionTime.value) + val response2: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(ownerToken) } @@ -170,6 +180,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() { @Test fun `contact cannot remove contacts`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) addContact(contact1) addContact(contact2) @@ -177,6 +188,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() { authenticateWithToken(contact2.token) } assertEquals(Unauthorized, response1.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response2: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(ownerToken) @@ -207,6 +219,7 @@ class ContactsManagerIntegrationTest : IntegrationTest() { @Test fun `removing non-existent contacts fails gracefully`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) addContact(getNewRandomContact(1)) addContact(getNewRandomContact(2)) @@ -215,6 +228,9 @@ class ContactsManagerIntegrationTest : IntegrationTest() { } assertEquals(NotFound, response1.status) + // still registers owner connection + assertTimestampRecent(metadataManager.ownerConnectionTime.value) + val response2: HttpResponse = httpClient.get("$baseUrl/contacts") { authenticateWithToken(ownerToken) } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt index 8c1e09017b7bc922f9741a52a5c08b569c00df33..8b838ef95e9252d5df850fe8b8b865a2ed666849 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerIntegrationTest.kt @@ -9,6 +9,7 @@ import io.ktor.client.statement.readBytes import io.ktor.client.statement.readText import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking +import org.briarproject.mailbox.core.TestUtils.assertTimestampRecent import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.server.IntegrationTest import org.junit.jupiter.api.AfterEach @@ -32,6 +33,11 @@ class FileManagerIntegrationTest : IntegrationTest() { @AfterEach fun cleanUp() { testComponent.getFileManager().deleteAllFiles() + db.write { txn -> + db.clearDatabase(txn) + // clears [metadataManager.ownerConnectionTime] + metadataManager.onDatabaseOpened(txn) + } } @Test @@ -64,12 +70,15 @@ class FileManagerIntegrationTest : IntegrationTest() { @Test fun `post new file, list, download and delete it`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) // owner uploads the file val response: HttpResponse = httpClient.post("$baseUrl/files/${contact1.inboxId}") { authenticateWithToken(ownerToken) body = bytes } assertEquals(HttpStatusCode.OK, response.status) + // owner connection got registered + assertTimestampRecent(metadataManager.ownerConnectionTime.value) // contact can list the file val listResponse: HttpResponse = httpClient.get("$baseUrl/files/${contact1.inboxId}") { @@ -105,10 +114,12 @@ class FileManagerIntegrationTest : IntegrationTest() { @Test fun `list files rejects wrong token`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response: HttpResponse = httpClient.get("$baseUrl/files/${getNewRandomId()}") { authenticateWithToken(token) } assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) // upload a real file val postResponse: HttpResponse = httpClient.post("$baseUrl/files/${contact1.inboxId}") { @@ -148,11 +159,14 @@ class FileManagerIntegrationTest : IntegrationTest() { @Test fun `list files gives empty response for empty folder`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response: HttpResponse = httpClient.get("$baseUrl/files/${contact1.outboxId}") { authenticateWithToken(ownerToken) } assertEquals(HttpStatusCode.OK, response.status) assertEquals("""{"files":[]}""", response.readText()) + // owner connection got registered + assertTimestampRecent(metadataManager.ownerConnectionTime.value) } @Test @@ -200,6 +214,17 @@ class FileManagerIntegrationTest : IntegrationTest() { assertEquals(HttpStatusCode.NotFound, response.status) } + @Test + fun `get file for contact does not update owner timestamp`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) + val id = getNewRandomId() + val response: HttpResponse = httpClient.get("$baseUrl/files/${contact1.inboxId}/$id") { + authenticateWithToken(contact1.token) + } + assertEquals(HttpStatusCode.NotFound, response.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) + } + @Test fun `delete file rejects wrong token`(): Unit = runBlocking { val response: HttpResponse = @@ -238,32 +263,52 @@ class FileManagerIntegrationTest : IntegrationTest() { @Test fun `delete file gives 404 response for unknown file`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val id = getNewRandomId() val response: HttpResponse = httpClient.delete("$baseUrl/files/${contact1.outboxId}/$id") { authenticateWithToken(ownerToken) } assertEquals(HttpStatusCode.NotFound, response.status) + // owner connection still got registered + assertTimestampRecent(metadataManager.ownerConnectionTime.value) + } + + @Test + fun `delete file for contact does not update owner timestamp`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) + val id = getNewRandomId() + val response: HttpResponse = httpClient.delete("$baseUrl/files/${contact1.inboxId}/$id") { + authenticateWithToken(contact1.token) + } + assertEquals(HttpStatusCode.NotFound, response.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) } @Test fun `list folders rejects contacts`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response: HttpResponse = httpClient.get("$baseUrl/folders") { authenticateWithToken(contact1.token) } assertEquals(HttpStatusCode.Unauthorized, response.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) } @Test fun `list folders allows owner, returns empty result`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) val response: HttpResponse = httpClient.get("$baseUrl/folders") { authenticateWithToken(ownerToken) } assertEquals(HttpStatusCode.OK, response.status) assertEquals("""{"folders":[]}""", response.readText()) + // owner connection got registered + assertTimestampRecent(metadataManager.ownerConnectionTime.value) } @Test fun `list folders returns more than a single folder`(): Unit = runBlocking { + assertEquals(0L, metadataManager.ownerConnectionTime.value) // contact1 uploads a file val response1: HttpResponse = httpClient.post("$baseUrl/files/${contact1.outboxId}") { authenticateWithToken(contact1.token) @@ -277,6 +322,7 @@ class FileManagerIntegrationTest : IntegrationTest() { body = bytes } assertEquals(HttpStatusCode.OK, response2.status) + assertEquals(0L, metadataManager.ownerConnectionTime.value) // owner now sees both contacts' outboxes in folders list val folderListResponse: FolderListResponse = httpClient.get("$baseUrl/folders") { @@ -284,5 +330,7 @@ class FileManagerIntegrationTest : IntegrationTest() { } val folderList = setOf(FolderResponse(contact1.outboxId), FolderResponse(contact2.outboxId)) assertEquals(folderList, folderListResponse.folders.toSet()) + // owner connection got registered + assertTimestampRecent(metadataManager.ownerConnectionTime.value) } } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt index 4501fcf4ffa037ae38d1bbdac0ca243f5151c1d9..7de249672059ee6714428009534880e16c256bea 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/AuthManagerTest.kt @@ -1,6 +1,8 @@ package org.briarproject.mailbox.core.server +import io.mockk.Runs import io.mockk.every +import io.mockk.just import io.mockk.mockk import org.briarproject.mailbox.core.TestUtils.everyRead import org.briarproject.mailbox.core.TestUtils.getNewRandomContact @@ -8,6 +10,7 @@ import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.db.Database import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal import org.briarproject.mailbox.core.server.MailboxPrincipal.SetupPrincipal +import org.briarproject.mailbox.core.settings.MetadataManager import org.briarproject.mailbox.core.setup.SetupManager import org.briarproject.mailbox.core.system.InvalidIdException import org.briarproject.mailbox.core.system.RandomIdManager @@ -22,9 +25,10 @@ class AuthManagerTest { private val db: Database = mockk() private val setupManager: SetupManager = mockk() + private val metadataManager: MetadataManager = mockk() private val randomIdManager = RandomIdManager() - private val authManager = AuthManager(db, setupManager, randomIdManager) + private val authManager = AuthManager(db, setupManager, metadataManager, randomIdManager) private val id = getNewRandomId() private val otherId = getNewRandomId() @@ -53,6 +57,7 @@ class AuthManagerTest { every { db.getContactWithToken(txn, id) } returns null every { setupManager.getOwnerToken(txn) } returns id } + every { metadataManager.onOwnerConnected() } just Runs assertEquals(OwnerPrincipal, authManager.getPrincipal(id)) } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt index 54752fac5091fd5ca352b3aa75a29efd6010471f..6cc47fc86c90a88124f59d10c1183298e42b0a8b 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/IntegrationTest.kt @@ -25,7 +25,9 @@ import java.io.File abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { protected lateinit var testComponent: TestComponent + protected val db by lazy { testComponent.getDatabase() } private val lifecycleManager by lazy { testComponent.getLifecycleManager() } + protected val metadataManager by lazy { testComponent.getMetadataManager() } protected val httpClient = HttpClient(CIO) { expectSuccess = false // prevents exceptions on non-success responses if (installJsonFeature) { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/StatusIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt similarity index 90% rename from mailbox-core/src/test/java/org/briarproject/mailbox/core/server/StatusIntegrationTest.kt rename to mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt index 2439ef58a6690482d36b4ed24bd65a94e8a5b9a4..d758217f86d36d3581d142ea09cfc87988afe4ef 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/server/StatusIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt @@ -1,15 +1,16 @@ -package org.briarproject.mailbox.core.server +package org.briarproject.mailbox.core.settings import io.ktor.client.request.get import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.readText import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking +import org.briarproject.mailbox.core.server.IntegrationTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals -class StatusIntegrationTest : IntegrationTest() { +class MetadataRouteManagerTest : IntegrationTest() { @BeforeEach fun initDb() { diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt index ca509825b8d0686e90acfce4ce91be2f601bc21e..9f28cace4fb3edff0e37cce31ccb319a9c9ea7cc 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/SetupManagerTest.kt @@ -13,13 +13,16 @@ import kotlin.test.assertNull class SetupManagerTest : IntegrationTest() { - private val db by lazy { testComponent.getDatabase() } private val setupManager by lazy { testComponent.getSetupManager() } @AfterEach fun resetToken() { - // re-set both token to not interfere with other tests - setupManager.setToken(null, null) + db.write { txn -> + // re-set both token to not interfere with other tests + db.clearDatabase(txn) + // clears [metadataManager.ownerConnectionTime] + metadataManager.onDatabaseOpened(txn) + } } @Test diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt index a4d847aab563c9efce1f22ead183da3ad3d6b634..b7c606099c6626550ebb86c3e5187b336011c0aa 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt @@ -14,8 +14,6 @@ import kotlin.test.assertTrue class WipeManagerTest : IntegrationTest() { - private val db by lazy { testComponent.getDatabase() } - @Test fun `wipe request rejects non-owners`() = runBlocking { addOwnerToken()