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 015bbe053c8f303e057b0afa46e94bce10a7520b..72433090a790f17ccfdb390e2c2002382c37f192 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 0f129e57150a358dc79df73ccfd7b709fde568c6..93eaea88bdc8075b073663ba7e9facaaf8e319c5 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 db4d9c81771f55859d48f429bfebb3ab6a4c5488..845336747fb79bc98b8524532c0a03c0db9ceb6b 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 8f99672c52e801d3ffd3b3045ee5c2cbd4463648..b5c7cfce62e900edbc85fda8471af2741b47c631 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 858b612887bdbf9c345ec20b8718e2a796763e70..2036c4cfea515dc625ccffc74bf03f90fe577256 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 ae7ec1f9ebe644056b91c04880c7631890fdeb83..6ba18129c44bed4ca64685ea136d7649f5b589a9 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 0d3ffe3bea9b09f39a2f4ad439155f4a688fe0ab..e7c7ec23dc09f7ab4fdd209b0803665eae8df41e 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 05f5db54d1958b3da471223b42128ec6fed7e973..04f52a233148173c32bdecd81d0e121978fa1377 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 27654404856566c46e62b7e2322f3ff60a982019..f79710f7559ab2d681468d483b57e71457ae6eac 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 0435916693ded6383645c9c6ae7554f9ad8e52c3..60616eed08d93b64397e0ebaa7af55684c0a6c40 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 aa83f0172ae07e6d95200a98135538347f0a4cf8..d9d92a47aecfdbdf6164e583d8ce2eff2a429c1b 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 5b5ce78539a57b495dd68329eb35753c43bf1afd..2ee5f0095b353ead8fc228249b38ed5227311f2c 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 c87f4ca2bc9a1a681f850538c8f02420d8f36e98..1c0d648588decbe62a4111bed4628503a5b24cd0 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 539a39dd52a7eb10a632275c5145fc22fc01ef66..e0ad498eb2764b4fd53066fb45a2cbf2f7feda97 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 9b270b45e2c69a0a5783fc7659c1fff0d1b7b140..2a1adc06bdf8bc09bb97f6dd91fae266dd1cdf42 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 acfd7ef9d5d97fa331ad47552471735e476861bf..ffa34af39653988f58da63d9a0d8849287791240 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 677fe04001cf36a224278c5d23c49004725e85ea..0f7456792197b2a6bd14f3f0ec549b1f4dc470c5 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 0000000000000000000000000000000000000000..1ea56cebc077cdfcfa8c26a30c162772f1c61858 --- /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 0000000000000000000000000000000000000000..165c75bebe5c3f20afd0821e2c4799eb1759944f --- /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 8b838ef95e9252d5df850fe8b8b865a2ed666849..de2f1d0461bd0ded29e70e91c0a353160794caec 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 49cba822da140b9eaead72788b1b1986d2acf890..4a89977ea39b6c4cc51dccf8c948bfb30538a505 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 d07a815ceec4fb13cff9791f73413cebec95d63d..10afd983ebd1403e75c3a426035ff35d0a80efdb 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 2e1a25b145e48abdc628e3ff583de98748f9cbd8..726a6f8cbf6cd9ce343b04c04cdc60e5e64de7d4 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 9f28cace4fb3edff0e37cce31ccb319a9c9ea7cc..83e40b887e92326ca36b829f853641d6726cbe9a 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 0000000000000000000000000000000000000000..6991e98dd4dbc0544bae03f3327974455a46f447 --- /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 b7c606099c6626550ebb86c3e5187b336011c0aa..984af156d0059376039d3ce0b0d83397210c3857 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) + } + ) } }