Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@ package com.flipcash.app.persistence

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNotSame
import kotlin.test.assertSame
import kotlin.test.assertTrue
Expand Down Expand Up @@ -50,8 +60,78 @@ class FlipcashDatabaseInitTest {
assertTrue(second.isOpen)
}

@Test
fun `suspending query against the outgoing database fails after an account switch`() {
runBlocking {
FlipcashDatabase.init(context, ENTROPY_A)
val outgoing = FlipcashDatabase.requireInstance()
val outgoingDao = outgoing.tokenDao()
// Proves the DAO and its connection pool are live before the switch.
assertTrue(outgoingDao.getAll().isEmpty())

FlipcashDatabase.init(context, ENTROPY_B)

assertFalse(outgoing.isOpen)
// The incoming database is usable straight away.
assertTrue(FlipcashDatabase.requireInstance().tokenDao().getAll().isEmpty())

// Room 2.8.5 made suspending queries throw IllegalStateException once the
// database is closed; in practice close() cancels Room's internal scope
// first, so the query fails with a CancellationException instead. Either
// way it must fail rather than read through a closed pool, so this asserts
// the failure and not its type.
val error = runCatching { outgoingDao.getAll() }.exceptionOrNull()
assertNotNull(error, "query against the closed outgoing database should fail")
}
}

@Test
fun `account switch does not fail a live collector on the outgoing database`() {
runBlocking {
FlipcashDatabase.init(context, ENTROPY_A)
val outgoingDao = FlipcashDatabase.requireInstance().tokenDao()

val firstEmission = CountDownLatch(1)
val failed = CountDownLatch(1)
val failure = AtomicReference<Throwable?>(null)
val collector = CoroutineScope(Dispatchers.IO).launch {
outgoingDao.observeAll()
.catch {
failure.set(it)
failed.countDown()
}
.collect { firstEmission.countDown() }
}

try {
assertTrue(
firstEmission.await(TIMEOUT_SECONDS, TimeUnit.SECONDS),
"collector never received an emission from the outgoing database",
)

// Switching accounts closes the outgoing database underneath the live
// collector. A screen still observing the old account must not be handed
// an exception, whatever Room does to the invalidation tracker.
FlipcashDatabase.init(context, ENTROPY_B)

assertFalse(
failed.await(QUIET_SECONDS, TimeUnit.SECONDS),
"collector on the outgoing database failed with ${failure.get()}",
)
} finally {
collector.cancel()
}
}
}

private companion object {
const val ENTROPY_A = "aaaaaaaaaaaaaaaaaaaaaaaa"
const val ENTROPY_B = "bbbbbbbbbbbbbbbbbbbbbbbb"

/** Upper bound on waiting for something that should happen. */
const val TIMEOUT_SECONDS = 10L

/** How long to wait to be satisfied that a failure is not coming. */
const val QUIET_SECONDS = 2L
}
}
Loading