From ae6dee005b25f7f2e3feffb5ee4cdf5a407d7b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20K=C3=BCrten?= <sebastian@mobanisto.de> Date: Wed, 10 Nov 2021 18:59:56 +0100 Subject: [PATCH] Implement lifecycle method for wiping the mailbox --- .../mailbox/android/MailboxViewModel.kt | 11 ++- .../mailbox/android/MainFragment.kt | 7 +- .../src/main/res/layout/fragment_main.xml | 12 +++ .../src/main/res/values/strings.xml | 1 + .../briarproject/mailbox/core/db/Database.kt | 5 +- .../mailbox/core/db/H2Database.kt | 74 +++++++++++----- .../mailbox/core/db/JdbcDatabase.kt | 48 ++++++++--- .../mailbox/core/files/FileManager.kt | 33 +++---- .../core/lifecycle/LifecycleManager.java | 18 +++- .../core/lifecycle/LifecycleManagerImpl.kt | 85 ++++++++++++++++--- .../mailbox/core/server/Routing.kt | 20 ++--- .../mailbox/core/server/WebServerManager.kt | 12 +-- .../mailbox/core/setup/WipeManager.kt | 32 ++++++- .../mailbox/core/TestComponent.kt | 2 + .../briarproject/mailbox/core/TestModule.kt | 4 +- .../ContactsManagerIntegrationTest.kt | 13 +-- ...ctsManagerMalformedInputIntegrationTest.kt | 12 +-- .../mailbox/core/db/TestDatabaseModule.kt | 25 ++++++ .../mailbox/core/db/TestH2Database.kt | 23 +++++ .../core/files/FileManagerIntegrationTest.kt | 11 +-- ...ManagerTest.kt => FileRouteManagerTest.kt} | 6 +- .../mailbox/core/server/IntegrationTest.kt | 17 ++++ .../core/settings/MetadataRouteManagerTest.kt | 11 +-- .../mailbox/core/setup/SetupManagerTest.kt | 11 --- .../core/setup/WipeRouteManagerTest.kt | 31 +++++++ ...rTest.kt => WipingWipeRouteManagerTest.kt} | 62 ++++++++------ 26 files changed, 418 insertions(+), 168 deletions(-) create mode 100644 mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt create mode 100644 mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt rename mailbox-core/src/test/java/org/briarproject/mailbox/core/files/{FileManagerTest.kt => FileRouteManagerTest.kt} (93%) create mode 100644 mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt rename mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/{WipeManagerTest.kt => WipingWipeRouteManagerTest.kt} (54%) diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt index 015bbe05..72433090 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.StateFlow import org.briarproject.android.dontkillmelib.DozeHelper import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState +import org.briarproject.mailbox.core.system.AndroidWakeLockManager import org.briarproject.mailbox.core.system.DozeWatchdog import javax.inject.Inject @@ -20,7 +21,8 @@ class MailboxViewModel @Inject constructor( private val dozeHelper: DozeHelper, private val dozeWatchdog: DozeWatchdog, handle: SavedStateHandle, - lifecycleManager: LifecycleManager, + private val lifecycleManager: LifecycleManager, + private val wakeLockManager: AndroidWakeLockManager, ) : AndroidViewModel(app) { val needToShowDoNotKillMeFragment get() = dozeHelper.needToShowDoNotKillMeFragment(app) @@ -46,6 +48,13 @@ class MailboxViewModel @Inject constructor( MailboxService.stopService(getApplication()) } + fun wipe() { + wakeLockManager.executeWakefully({ + lifecycleManager.wipeMailbox() + MailboxService.stopService(getApplication()) + }, "LifecycleWipe") + } + fun getAndResetDozeFlag() = dozeWatchdog.andResetDozeFlag fun updateText(str: String) { diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt index 0f129e57..93eaea88 100644 --- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt +++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MainFragment.kt @@ -23,6 +23,7 @@ class MainFragment : Fragment() { private val viewModel: MailboxViewModel by activityViewModels() private lateinit var statusTextView: TextView private lateinit var startStopButton: Button + private lateinit var wipeButton: Button override fun onCreateView( inflater: LayoutInflater, @@ -37,6 +38,7 @@ class MainFragment : Fragment() { val button = v.findViewById<Button>(R.id.button) statusTextView = v.findViewById(R.id.statusTextView) startStopButton = v.findViewById(R.id.startStopButton) + wipeButton = v.findViewById(R.id.wipeButton) button.setOnClickListener { viewModel.updateText("Tested") @@ -60,7 +62,7 @@ class MainFragment : Fragment() { } private fun onLifecycleStateChanged(state: LifecycleManager.LifecycleState) = when (state) { - LifecycleManager.LifecycleState.STOPPED -> { + LifecycleManager.LifecycleState.NOT_STARTED -> { statusTextView.text = state.name startStopButton.setText(R.string.start) startStopButton.setOnClickListener { viewModel.startLifecycle() } @@ -70,11 +72,14 @@ class MainFragment : Fragment() { statusTextView.text = state.name startStopButton.setText(R.string.stop) startStopButton.setOnClickListener { viewModel.stopLifecycle() } + wipeButton.setOnClickListener { viewModel.wipe() } startStopButton.isEnabled = true + wipeButton.isEnabled = true } else -> { statusTextView.text = state.name startStopButton.isEnabled = false + wipeButton.isEnabled = false } } diff --git a/mailbox-android/src/main/res/layout/fragment_main.xml b/mailbox-android/src/main/res/layout/fragment_main.xml index db4d9c81..84533674 100644 --- a/mailbox-android/src/main/res/layout/fragment_main.xml +++ b/mailbox-android/src/main/res/layout/fragment_main.xml @@ -46,6 +46,18 @@ android:layout_margin="16dp" android:text="@string/start" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/wipeButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/wipeButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:enabled="false" + android:text="@string/wipe" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/mailbox-android/src/main/res/values/strings.xml b/mailbox-android/src/main/res/values/strings.xml index 8f99672c..b5c7cfce 100644 --- a/mailbox-android/src/main/res/values/strings.xml +++ b/mailbox-android/src/main/res/values/strings.xml @@ -5,6 +5,7 @@ <string name="notification_mailbox_content">Waiting for messages…</string> <string name="start">Start mailbox</string> <string name="stop">Stop mailbox</string> + <string name="wipe">Wipe mailbox</string> <!-- TODO: We might want to copy string from don't kill me lib, so translation memory can auto-translate most of them. --> diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt index 858b6128..2036c4cf 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/Database.kt @@ -6,7 +6,8 @@ import org.briarproject.mailbox.core.settings.Settings interface Database : TransactionManager { /** - * Opens the database and returns true if the database already existed. + * Opens the database and returns true if the database already existed. Existence of the + * database is defined as the database files exists and the database contains a valid schema. */ fun open(listener: MigrationListener?): Boolean @@ -18,7 +19,7 @@ interface Database : TransactionManager { fun close() @Throws(DbException::class) - fun clearDatabase(txn: Transaction) + fun dropAllTablesAndClose() @Throws(DbException::class) fun getSettings(txn: Transaction, namespace: String): Settings diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt index ae7ec1f9..6ba18129 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/H2Database.kt @@ -3,18 +3,20 @@ package org.briarproject.mailbox.core.db import org.briarproject.mailbox.core.db.JdbcUtils.tryToClose import org.briarproject.mailbox.core.system.Clock import org.briarproject.mailbox.core.util.IoUtils.isNonEmptyDirectory +import org.briarproject.mailbox.core.util.LogUtils.info import org.briarproject.mailbox.core.util.LogUtils.logFileOrDir import org.slf4j.LoggerFactory import java.io.File import java.sql.Connection import java.sql.DriverManager +import java.sql.ResultSet import java.sql.SQLException import java.sql.Statement import java.util.Properties -class H2Database( +open class H2Database( private val config: DatabaseConfig, - val clock: Clock, + clock: Clock, ) : JdbcDatabase(dbTypes, clock) { internal companion object { @@ -35,32 +37,58 @@ class H2Database( override fun open(listener: MigrationListener?): Boolean { val dir = config.getDatabaseDirectory() - if (LOG.isInfoEnabled) { - LOG.info("Contents of account directory before opening DB:") - logFileOrDir(LOG, dir.parentFile) - } - val reopen = isNonEmptyDirectory(dir) - if (LOG.isInfoEnabled) LOG.info("Reopening DB: $reopen") - if (!reopen && dir.mkdirs()) LOG.info("Created database directory") - super.open("org.h2.Driver", reopen, listener) - if (LOG.isInfoEnabled) { - LOG.info("Contents of account directory after opening DB:") - logFileOrDir(LOG, dir.parentFile) - } + LOG.info { "Contents of account directory before opening DB:" } + logFileOrDir(LOG, dir.parentFile) + val databaseDirNonEmpty = isNonEmptyDirectory(dir) + if (!databaseDirNonEmpty && dir.mkdirs()) LOG.info("Created database directory") + val reopen = super.open("org.h2.Driver", listener) + LOG.info { "Contents of account directory after opening DB:" } + logFileOrDir(LOG, dir.parentFile) return reopen } + override fun databaseHasSettingsTable(): Boolean { + return read { txn -> + val connection = txn.unbox() + var tables: ResultSet? = null + try { + // Need to check for PUBLIC schema as there is another table called SETTINGS on the + // INFORMATION_SCHEMA schema. + tables = connection.metaData.getTables(null, "PUBLIC", "SETTINGS", null) + // if that query returns any rows, the settings table does exist + tables.next() + } catch (e: SQLException) { + LOG.warn("Error while checking for settings table existence", e) + tryToClose(tables, LOG) + false + } + } + } + override fun close() { - // H2 will close the database when the last connection closes - var c: Connection? = null + connectionsLock.lock() try { - c = createConnection() - super.closeAllConnections() - setDirty(c, false) - c.close() - } catch (e: SQLException) { - tryToClose(c, LOG) - throw DbException(e) + // This extra check is mainly added for tests where we might have closed the database + // already by resetting the database after each test and then the lifecycle manager + // tries to close again. However closing an already closed database doesn't make + // sense, also in production, so bail out quickly here. + // This is important especially after the database has been cleared, because the + // settings table is gone and if we allowed the flow to continue further, we would try + // to store the dirty flag in the no longer existing settings table. + if (closed) return + // H2 will close the database when the last connection closes + var c: Connection? = null + try { + c = createConnection() + super.closeAllConnections() + setDirty(c, false) + c.close() + } catch (e: SQLException) { + tryToClose(c, LOG) + throw DbException(e) + } + } finally { + connectionsLock.unlock() } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt index 0d3ffe3b..e7c7ec23 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/db/JdbcDatabase.kt @@ -9,6 +9,7 @@ import org.briarproject.mailbox.core.db.DatabaseConstants.Companion.SCHEMA_VERSI import org.briarproject.mailbox.core.db.JdbcUtils.tryToClose import org.briarproject.mailbox.core.settings.Settings import org.briarproject.mailbox.core.system.Clock +import org.briarproject.mailbox.core.util.LogUtils.info import org.briarproject.mailbox.core.util.LogUtils.logDuration import org.briarproject.mailbox.core.util.LogUtils.logException import org.briarproject.mailbox.core.util.LogUtils.now @@ -50,7 +51,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc """.trimIndent() } - private val connectionsLock: Lock = ReentrantLock() + protected val connectionsLock: Lock = ReentrantLock() private val connectionsChanged = connectionsLock.newCondition() @GuardedBy("connectionsLock") @@ -60,21 +61,29 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc private var openConnections = 0 @GuardedBy("connectionsLock") - private var closed = false + protected var closed = false @Volatile private var wasDirtyOnInitialisation = false private val lock = ReentrantReadWriteLock(true) - fun open(driverClass: String, reopen: Boolean, listener: MigrationListener?) { - // Load the JDBC driver + /* + * Returns true if the database already existed + */ + internal fun open(driverClass: String, listener: MigrationListener?): Boolean { try { Class.forName(driverClass) } catch (e: ClassNotFoundException) { throw DbException(e) } // Open the database and create the tables and indexes if necessary + LOG.info { "checking for settings table" } + val reopen = databaseHasSettingsTable() + LOG.info { + if (reopen) "settings table found, reopening" + else "settings table not found, not reopening" + } var compact = false write { txn -> val connection = txn.unbox() @@ -88,9 +97,7 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc initialiseSettings(connection) false } - if (LOG.isInfoEnabled) { - LOG.info("db dirty? $wasDirtyOnInitialisation") - } + LOG.info { "db dirty? $wasDirtyOnInitialisation" } createIndexes(connection) } // Compact the database if necessary @@ -100,13 +107,22 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc compactAndClose() logDuration(LOG, { "Compacting database" }, start) // Allow the next transaction to reopen the DB - synchronized(connectionsLock) { closed = false } + connectionsLock.lock() + try { + closed = false + } finally { + connectionsLock.unlock() + } write { txn -> storeLastCompacted(txn.unbox()) } } + return reopen } + @Throws(DbException::class, SQLException::class) + protected abstract fun databaseHasSettingsTable(): Boolean + /** * Compares the schema version stored in the database with the schema * version used by the current code and applies any suitable migrations to @@ -352,10 +368,18 @@ abstract class JdbcDatabase(private val dbTypes: DatabaseTypes, private val cloc } @Throws(DbException::class) - override fun clearDatabase(txn: Transaction) { - val connection: Connection = txn.unbox() - execute(connection, "DELETE FROM settings") - execute(connection, "DELETE FROM contacts") + override fun dropAllTablesAndClose() { + connectionsLock.lock() + try { + write { txn -> + val connection: Connection = txn.unbox() + execute(connection, "DROP TABLE settings") + execute(connection, "DROP TABLE contacts") + } + closeAllConnections() + } finally { + connectionsLock.unlock() + } } private fun execute(connection: Connection, sql: String) { diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt index 05f5db54..04f52a23 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/files/FileManager.kt @@ -20,12 +20,29 @@ import javax.inject.Inject private val LOG = getLogger(FileManager::class.java) class FileManager @Inject constructor( + private val fileProvider: FileProvider, +) { + fun deleteAllFiles(): Boolean { + var allDeleted = true + fileProvider.folderRoot.listFiles()?.forEach { folder -> + if (!folder.deleteRecursively()) { + allDeleted = false + LOG.warn("Not everything in $folder could get deleted.") + } + } ?: run { + allDeleted = false + LOG.warn("Could not delete folders.") + } + return allDeleted + } +} + +class FileRouteManager @Inject constructor( private val db: Database, private val authManager: AuthManager, private val fileProvider: FileProvider, private val randomIdManager: RandomIdManager, ) { - /** * Used by contacts to send files to the owner and by the owner to send files to contacts. * @@ -137,20 +154,6 @@ class FileManager @Inject constructor( } call.respond(folderListResponse) } - - fun deleteAllFiles(): Boolean { - var allDeleted = true - fileProvider.folderRoot.listFiles()?.forEach { folder -> - if (!folder.deleteRecursively()) { - allDeleted = false - LOG.warn("Not everything in $folder could get deleted.") - } - } ?: run { - allDeleted = false - LOG.warn("Could not delete folders.") - } - return allDeleted - } } data class FileListResponse(val files: List<FileResponse>) 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 27654404..f79710f7 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 @@ -30,13 +30,15 @@ public interface LifecycleManager { */ enum LifecycleState { - STOPPED, + NOT_STARTED, STARTING, MIGRATING_DATABASE, COMPACTING_DATABASE, STARTING_SERVICES, RUNNING, - STOPPING; + WIPING, + STOPPING, + STOPPED; public boolean isAfter(LifecycleState state) { return ordinal() > state.ordinal(); @@ -77,6 +79,15 @@ public interface LifecycleManager { @Wakeful void stopServices(); + /** + * Wipes entire database as well as stored files. Also stops all services + * by launching stopServices() on a new thread after wiping completes. + * + * @return true if wiping was successful, false otherwise + */ + @Wakeful + boolean wipeMailbox(); + /** * Waits for the {@link Database} to be opened before returning. */ @@ -106,6 +117,9 @@ public interface LifecycleManager { /** * Called when the database is being opened, before * {@link #waitForDatabase()} returns. + * <p> + * Don't call any methods from the {@link LifecycleManager} here as + * this is most likely going to end up in a deadlock. */ @Wakeful 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 04359166..60616eed 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 @@ -7,16 +7,19 @@ import org.briarproject.mailbox.core.db.MigrationListener import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.COMPACTING_DATABASE import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.MIGRATING_DATABASE +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.NOT_STARTED import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.RUNNING import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STARTING_SERVICES import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPED import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.STOPPING +import org.briarproject.mailbox.core.lifecycle.LifecycleManager.LifecycleState.WIPING import org.briarproject.mailbox.core.lifecycle.LifecycleManager.OpenDatabaseHook import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS +import org.briarproject.mailbox.core.setup.WipeManager import org.briarproject.mailbox.core.util.LogUtils.info import org.briarproject.mailbox.core.util.LogUtils.logDuration import org.briarproject.mailbox.core.util.LogUtils.logException @@ -27,11 +30,16 @@ import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CountDownLatch import java.util.concurrent.ExecutorService import java.util.concurrent.Semaphore +import javax.annotation.concurrent.GuardedBy import javax.annotation.concurrent.ThreadSafe import javax.inject.Inject +import kotlin.concurrent.thread @ThreadSafe -internal class LifecycleManagerImpl @Inject constructor(private val db: Database) : +internal class LifecycleManagerImpl @Inject constructor( + private val db: Database, + private val wipeManager: WipeManager, +) : LifecycleManager, MigrationListener { companion object { @@ -41,11 +49,15 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database private val services: MutableList<Service> private val openDatabaseHooks: MutableList<OpenDatabaseHook> private val executors: MutableList<ExecutorService> - private val startStopSemaphore = Semaphore(1) + + // This semaphore makes sure that startServices(), stopServices() and wipeMailbox() + // do not run concurrently. Also all write access to 'state' must happen while this + // semaphore is being held. + private val startStopWipeSemaphore = Semaphore(1) private val dbLatch = CountDownLatch(1) private val startupLatch = CountDownLatch(1) private val shutdownLatch = CountDownLatch(1) - private val state = MutableStateFlow(STOPPED) + private val state = MutableStateFlow(NOT_STARTED) init { services = CopyOnWriteArrayList() @@ -68,12 +80,13 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database executors.add(e) } + @GuardedBy("startStopWipeSemaphore") override fun startServices(): StartResult { - if (!startStopSemaphore.tryAcquire()) { + if (!startStopWipeSemaphore.tryAcquire()) { LOG.info("Already starting or stopping") return ALREADY_RUNNING } - state.compareAndSet(STOPPED, STARTING) + state.compareAndSet(NOT_STARTED, STARTING) return try { LOG.info("Opening database") var start = now() @@ -101,26 +114,37 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database logException(LOG, e) SERVICE_ERROR } finally { - startStopSemaphore.release() + startStopWipeSemaphore.release() } } + // startStopWipeSemaphore is being held during this because it will be called during db.open() + // in startServices() + @GuardedBy("startStopWipeSemaphore") override fun onDatabaseMigration() { state.value = MIGRATING_DATABASE } + // startStopWipeSemaphore is being held during this because it will be called during db.open() + // in startServices() + @GuardedBy("startStopWipeSemaphore") override fun onDatabaseCompaction() { state.value = COMPACTING_DATABASE } + @GuardedBy("startStopWipeSemaphore") override fun stopServices() { try { - startStopSemaphore.acquire() + startStopWipeSemaphore.acquire() } catch (e: InterruptedException) { LOG.warn("Interrupted while waiting to stop services") return } try { + if (state.value == STOPPED) { + return + } + val wiped = state.value == WIPING LOG.info("Stopping services") state.value = STOPPING for (s in services) { @@ -132,15 +156,54 @@ internal class LifecycleManagerImpl @Inject constructor(private val db: Database LOG.trace { "Stopping executor ${e.javaClass.simpleName}" } e.shutdownNow() } - val start = now() - db.close() - logDuration(LOG, { "Closing database" }, start) + if (wiped) { + // If we just wiped, the database has already been closed, so we should not call + // close(). Since the services are being shut down after wiping (so that the web + // server can still respond to a wipe request), it is possible that a concurrent + // API call created some files in the meantime. To make sure we delete those in + // case of a wipe, repeat deletion of files here after the services have been + // stopped. + wipeManager.wipe(wipeDatabase = false) + } else { + val start = now() + db.close() + logDuration(LOG, { "Closing database" }, start) + } shutdownLatch.countDown() } catch (e: ServiceException) { logException(LOG, e) } finally { - startStopSemaphore.release() state.compareAndSet(STOPPING, STOPPED) + startStopWipeSemaphore.release() + } + } + + @GuardedBy("startStopWipeSemaphore") + override fun wipeMailbox(): Boolean { + try { + startStopWipeSemaphore.acquire() + } catch (e: InterruptedException) { + LOG.warn("Interrupted while waiting to wipe mailbox") + return false + } + if (!state.compareAndSet(RUNNING, WIPING)) { + return false + } + try { + wipeManager.wipe(wipeDatabase = true) + + // We need to move this to a thread so that the webserver call can finish when it calls + // this. Otherwise we'll end up in a deadlock: the same thread trying to stop the + // webserver from within a call that wants to send a response on the very same webserver. + // If we were not do this, the webserver would wait for the request to finish and the + // request would wait for the webserver to finish. + thread { + stopServices() + } + + return true + } finally { + startStopWipeSemaphore.release() } } 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 aa83f017..d9d92a47 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 @@ -20,10 +20,10 @@ import io.ktor.routing.route 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.files.FileRouteManager 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.setup.WipeRouteManager import org.briarproject.mailbox.core.system.InvalidIdException internal const val V = "/" // TODO set to "/v1" for release @@ -31,7 +31,7 @@ internal const val V = "/" // TODO set to "/v1" for release internal fun Application.configureBasicApi( metadataRouteManager: MetadataRouteManager, setupRouteManager: SetupRouteManager, - wipeManager: WipeManager, + wipeRouteManager: WipeRouteManager, ) = routing { route(V) { get { @@ -49,7 +49,7 @@ internal fun Application.configureBasicApi( } delete { call.handle { - wipeManager.onWipeRequest(call) + wipeRouteManager.onWipeRequest(call) } } put("/setup") { @@ -85,18 +85,18 @@ internal fun Application.configureContactApi(contactsManager: ContactsManager) = } } -internal fun Application.configureFilesApi(fileManager: FileManager) = routing { +internal fun Application.configureFilesApi(fileRouteManager: FileRouteManager) = routing { authenticate { route("$V/files/{folderId}") { post { call.handle { - fileManager.postFile(call, call.parameters.getOrFail("folderId")) + fileRouteManager.postFile(call, call.parameters.getOrFail("folderId")) } } get { call.handle { - fileManager.listFiles(call, call.parameters.getOrFail("folderId")) + fileRouteManager.listFiles(call, call.parameters.getOrFail("folderId")) } } route("/{fileId}") { @@ -104,14 +104,14 @@ internal fun Application.configureFilesApi(fileManager: FileManager) = routing { val folderId = call.parameters.getOrFail("folderId") val fileId = call.parameters.getOrFail("fileId") call.handle { - fileManager.getFile(call, folderId, fileId) + fileRouteManager.getFile(call, folderId, fileId) } } delete { val folderId = call.parameters.getOrFail("folderId") val fileId = call.parameters.getOrFail("fileId") call.handle { - fileManager.deleteFile(call, folderId, fileId) + fileRouteManager.deleteFile(call, folderId, fileId) } } } @@ -120,7 +120,7 @@ internal fun Application.configureFilesApi(fileManager: FileManager) = routing { authenticate { get("$V/folders") { call.handle { - fileManager.listFoldersWithFiles(call) + fileRouteManager.listFoldersWithFiles(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 5b5ce785..2ee5f009 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 @@ -9,12 +9,12 @@ import io.ktor.jackson.jackson import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import org.briarproject.mailbox.core.contacts.ContactsManager -import org.briarproject.mailbox.core.files.FileManager +import org.briarproject.mailbox.core.files.FileRouteManager import org.briarproject.mailbox.core.lifecycle.Service import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT 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.setup.WipeRouteManager import org.slf4j.Logger import org.slf4j.LoggerFactory.getLogger import javax.inject.Inject @@ -32,8 +32,8 @@ internal class WebServerManagerImpl @Inject constructor( private val metadataRouteManager: MetadataRouteManager, private val setupRouteManager: SetupRouteManager, private val contactsManager: ContactsManager, - private val fileManager: FileManager, - private val wipeManager: WipeManager, + private val fileRouteManager: FileRouteManager, + private val wipeRouteManager: WipeRouteManager, ) : WebServerManager { internal companion object { @@ -57,9 +57,9 @@ internal class WebServerManagerImpl @Inject constructor( enable(BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES) } } - configureBasicApi(metadataRouteManager, setupRouteManager, wipeManager) + configureBasicApi(metadataRouteManager, setupRouteManager, wipeRouteManager) configureContactApi(contactsManager) - configureFilesApi(fileManager) + configureFilesApi(fileRouteManager) } } diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt index c87f4ca2..1c0d6485 100644 --- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt +++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/WipeManager.kt @@ -5,16 +5,39 @@ import io.ktor.auth.principal import io.ktor.http.HttpStatusCode import io.ktor.response.respond import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.db.DatabaseConfig import org.briarproject.mailbox.core.files.FileManager +import org.briarproject.mailbox.core.lifecycle.LifecycleManager import org.briarproject.mailbox.core.server.AuthException import org.briarproject.mailbox.core.server.MailboxPrincipal +import org.briarproject.mailbox.core.server.MailboxPrincipal.OwnerPrincipal +import org.briarproject.mailbox.core.util.IoUtils import javax.inject.Inject class WipeManager @Inject constructor( private val db: Database, + private val databaseConfig: DatabaseConfig, private val fileManager: FileManager, ) { + /* + * This must only be called by the LifecycleManager + */ + fun wipe(wipeDatabase: Boolean) { + if (wipeDatabase) { + db.dropAllTablesAndClose() + val dir = databaseConfig.getDatabaseDirectory() + IoUtils.deleteFileOrDir(dir) + } + fileManager.deleteAllFiles() + } + +} + +class WipeRouteManager @Inject constructor( + private val lifecycleManager: LifecycleManager, +) { + /** * Handler for `DELETE /` API endpoint. * @@ -24,12 +47,13 @@ class WipeManager @Inject constructor( @Throws(AuthException::class) suspend fun onWipeRequest(call: ApplicationCall) { val principal = call.principal<MailboxPrincipal>() - if (principal !is MailboxPrincipal.OwnerPrincipal) throw AuthException() + if (principal !is OwnerPrincipal) throw AuthException() - db.write { txn -> - db.clearDatabase(txn) + val wiped = lifecycleManager.wipeMailbox() + if (!wiped) { + call.respond(HttpStatusCode.InternalServerError) + return } - fileManager.deleteAllFiles() call.respond(HttpStatusCode.NoContent) } 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 539a39dd..e0ad498e 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 @@ -2,6 +2,7 @@ package org.briarproject.mailbox.core import dagger.Component import org.briarproject.mailbox.core.db.Database +import org.briarproject.mailbox.core.db.DatabaseConfig import org.briarproject.mailbox.core.files.FileManager import org.briarproject.mailbox.core.files.FileProvider import org.briarproject.mailbox.core.lifecycle.LifecycleManager @@ -23,6 +24,7 @@ interface TestComponent { fun getSettingsManager(): SettingsManager fun getSetupManager(): SetupManager fun getFileManager(): FileManager + fun getDatabaseConfig(): DatabaseConfig fun getDatabase(): Database fun getRandomIdManager(): RandomIdManager fun getFileProvider(): FileProvider diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt index 9b270b45..2a1adc06 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/TestModule.kt @@ -5,7 +5,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.briarproject.mailbox.core.db.DatabaseConfig -import org.briarproject.mailbox.core.db.DatabaseModule +import org.briarproject.mailbox.core.db.TestDatabaseModule import org.briarproject.mailbox.core.files.FileProvider import org.briarproject.mailbox.core.lifecycle.LifecycleModule import org.briarproject.mailbox.core.server.WebServerModule @@ -17,7 +17,7 @@ import javax.inject.Singleton @Module( includes = [ LifecycleModule::class, - DatabaseModule::class, + TestDatabaseModule::class, WebServerModule::class, SettingsModule::class, // no Tor module 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 acfd7ef9..ffa34af3 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 @@ -18,7 +18,6 @@ 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.server.IntegrationTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.math.max @@ -28,19 +27,11 @@ import kotlin.test.assertEquals class ContactsManagerIntegrationTest : IntegrationTest() { @BeforeEach - fun initDb() { + override fun initDb() { + super.initDb() addOwnerToken() } - @AfterEach - 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) diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt index 677fe040..0f745679 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/contacts/ContactsManagerMalformedInputIntegrationTest.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.TestUtils import org.briarproject.mailbox.core.TestUtils.getNewRandomContact import org.briarproject.mailbox.core.server.IntegrationTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.random.Random @@ -20,18 +19,11 @@ import kotlin.test.assertEquals class ContactsManagerMalformedInputIntegrationTest : IntegrationTest(false) { @BeforeEach - fun initDb() { + override fun initDb() { + super.initDb() addOwnerToken() } - @AfterEach - fun clearDb() { - val db = testComponent.getDatabase() - db.write { txn -> - db.clearDatabase(txn) - } - } - /** * This test is the same as the one from [ContactsManagerIntegrationTest], just that it supplies * raw JSON as a body. Unlike all other tests in this class, this one should be able to create diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt new file mode 100644 index 00000000..1ea56ceb --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestDatabaseModule.kt @@ -0,0 +1,25 @@ +package org.briarproject.mailbox.core.db + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.briarproject.mailbox.core.system.Clock +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class TestDatabaseModule { + + @Provides + @Singleton + fun provideDatabase(config: DatabaseConfig, clock: Clock): Database { + return TestH2Database(config, clock) + } + + @Provides + fun provideTransactionManager(db: Database): TransactionManager { + return db + } + +} diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt new file mode 100644 index 00000000..165c75be --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/db/TestH2Database.kt @@ -0,0 +1,23 @@ +package org.briarproject.mailbox.core.db + +import org.briarproject.mailbox.core.system.Clock + +class TestH2Database( + config: DatabaseConfig, + clock: Clock, +) : H2Database(config, clock) { + + /** + * A special version of open() for testing that allows reopening a database that has been closed. + */ + override fun open(listener: MigrationListener?): Boolean { + connectionsLock.lock() + try { + closed = false + } finally { + connectionsLock.unlock() + } + return super.open(listener) + } + +} 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 8b838ef9..de2f1d04 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 @@ -24,20 +24,17 @@ class FileManagerIntegrationTest : IntegrationTest() { private val bytes = Random.nextBytes(2048) @BeforeEach - fun initDb() { + override fun initDb() { + super.initDb() addOwnerToken() addContact(contact1) addContact(contact2) } @AfterEach - fun cleanUp() { + override fun clearDb() { + super.clearDb() testComponent.getFileManager().deleteAllFiles() - db.write { txn -> - db.clearDatabase(txn) - // clears [metadataManager.ownerConnectionTime] - metadataManager.onDatabaseOpened(txn) - } } @Test diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt similarity index 93% rename from mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt rename to mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt index 49cba822..4a89977e 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/files/FileRouteManagerTest.kt @@ -27,14 +27,14 @@ import kotlin.random.Random import kotlin.test.assertFalse import kotlin.test.assertTrue -class FileManagerTest { +class FileRouteManagerTest { private val db: Database = mockk() private val authManager: AuthManager = mockk() private val fileProvider: FileProvider = mockk() private val randomIdManager = RandomIdManager() - private val fileManager = FileManager(db, authManager, fileProvider, randomIdManager) + private val fileRouteManager = FileRouteManager(db, authManager, fileProvider, randomIdManager) private val call: ApplicationCall = mockk() private val id = getNewRandomId() @@ -58,7 +58,7 @@ class FileManagerTest { every { fileProvider.getFile(id, any()) } returns finalFile coEvery { call.respond(HttpStatusCode.OK) } just Runs - fileManager.postFile(call, id) + fileRouteManager.postFile(call, id) assertFalse(tmpFile.exists()) assertTrue(finalFile.exists()) 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 d07a815c..10afd983 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 @@ -17,7 +17,9 @@ import org.briarproject.mailbox.core.TestUtils.getNewRandomId import org.briarproject.mailbox.core.contacts.Contact import org.briarproject.mailbox.core.server.WebServerManager.Companion.PORT import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestInstance.Lifecycle import org.junit.jupiter.api.io.TempDir @@ -63,6 +65,21 @@ abstract class IntegrationTest(private val installJsonFeature: Boolean = true) { lifecycleManager.waitForShutdown() } + @BeforeEach + open fun initDb() { + // need to reopen database here because we're closing it after each test + db.open(null) + db.read { txn -> + // clears [metadataManager.ownerConnectionTime] + metadataManager.onDatabaseOpened(txn) + } + } + + @AfterEach + open fun clearDb() { + db.dropAllTablesAndClose() + } + protected fun addOwnerToken() { testComponent.getSetupManager().setToken(null, ownerToken) } diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt index 2e1a25b1..726a6f8c 100644 --- a/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/settings/MetadataRouteManagerTest.kt @@ -6,7 +6,6 @@ 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.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -14,19 +13,13 @@ import kotlin.test.assertEquals class MetadataRouteManagerTest : IntegrationTest() { @BeforeEach - fun initDb() { + override fun initDb() { + super.initDb() addOwnerToken() addContact(contact1) addContact(contact2) } - @AfterEach - fun clearDb() { - db.write { txn -> - db.clearDatabase(txn) - } - } - @Test fun `owner can access status`(): Unit = runBlocking { val response: HttpResponse = httpClient.get("$baseUrl/status") { 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 9f28cace..83e40b88 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 @@ -5,7 +5,6 @@ import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking import org.briarproject.mailbox.core.server.IntegrationTest -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -15,16 +14,6 @@ class SetupManagerTest : IntegrationTest() { private val setupManager by lazy { testComponent.getSetupManager() } - @AfterEach - fun resetToken() { - db.write { txn -> - // re-set both token to not interfere with other tests - db.clearDatabase(txn) - // clears [metadataManager.ownerConnectionTime] - metadataManager.onDatabaseOpened(txn) - } - } - @Test fun `restarting setup wipes owner token and creates setup token`() { // initially, there's no setup and no owner token diff --git a/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt new file mode 100644 index 00000000..6991e98d --- /dev/null +++ b/mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeRouteManagerTest.kt @@ -0,0 +1,31 @@ +package org.briarproject.mailbox.core.setup + +import io.ktor.client.request.delete +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.runBlocking +import org.briarproject.mailbox.core.server.IntegrationTest +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class WipeRouteManagerTest : IntegrationTest() { + + @Test + fun `wipe request rejects non-owners`() = runBlocking { + addOwnerToken() + addContact(contact1) + + // Unauthorized with random token + val response1 = httpClient.delete<HttpResponse>("$baseUrl/") { + authenticateWithToken(token) + } + assertEquals(HttpStatusCode.Unauthorized, response1.status) + + // Unauthorized with contact's token + val response2 = httpClient.delete<HttpResponse>("$baseUrl/") { + authenticateWithToken(contact1.token) + } + assertEquals(HttpStatusCode.Unauthorized, response2.status) + } + +} 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/WipingWipeRouteManagerTest.kt similarity index 54% rename from mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipeManagerTest.kt rename to mailbox-core/src/test/java/org/briarproject/mailbox/core/setup/WipingWipeRouteManagerTest.kt index b7c60609..984af156 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/WipingWipeRouteManagerTest.kt @@ -5,32 +5,17 @@ import io.ktor.client.request.post import io.ktor.client.statement.HttpResponse import io.ktor.http.HttpStatusCode import kotlinx.coroutines.runBlocking +import org.briarproject.mailbox.core.db.DbException import org.briarproject.mailbox.core.server.IntegrationTest import org.junit.jupiter.api.Test import kotlin.random.Random import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -class WipeManagerTest : IntegrationTest() { - - @Test - fun `wipe request rejects non-owners`() = runBlocking { - addOwnerToken() - addContact(contact1) - - // Unauthorized with random token - val response1 = httpClient.delete<HttpResponse>("$baseUrl/") { - authenticateWithToken(token) - } - assertEquals(HttpStatusCode.Unauthorized, response1.status) - - // Unauthorized with contact's token - val response2 = httpClient.delete<HttpResponse>("$baseUrl/") { - authenticateWithToken(contact1.token) - } - assertEquals(HttpStatusCode.Unauthorized, response2.status) - } +class WipingWipeRouteManagerTest : IntegrationTest() { @Test fun `wipe request deletes files and db for owner`() = runBlocking { @@ -51,19 +36,40 @@ class WipeManagerTest : IntegrationTest() { } assertEquals(HttpStatusCode.NoContent, response.status) - // no more contacts in DB - val contacts = db.read { db.getContacts(it) } - assertEquals(0, contacts.size) - - // owner token was cleared as well - val token = db.read { txn -> - testComponent.getSetupManager().getOwnerToken(txn) - } - assertNull(token) + // assert that database is gone + assertFalse(testComponent.getDatabaseConfig().getDatabaseDirectory().exists()) // no more files are stored val folderRoot = testComponent.getFileProvider().folderRoot assertTrue(folderRoot.listFiles()?.isEmpty() ?: false) + + // no more contacts in DB - contacts table is gone + // it actually fails because db is closed though + assertFailsWith<DbException> { db.read { db.getContacts(it) } } + + // owner token was cleared as well - settings table is gone + // it actually fails because db is closed though + assertFailsWith<DbException> { + db.read { txn -> + testComponent.getSetupManager().getOwnerToken(txn) + } + } + + // re-open the database + db.open(null) + + // reopening re-created the database directory + assertTrue(testComponent.getDatabaseConfig().getDatabaseDirectory().exists()) + + // no more contacts in DB + assertTrue(db.read { db.getContacts(it) }.isEmpty()) + + // owner token was cleared as well + assertNull( + db.read { txn -> + testComponent.getSetupManager().getOwnerToken(txn) + } + ) } } -- GitLab