From 62cec632ef01e98931b9388daddd83dae808cf9a Mon Sep 17 00:00:00 2001 From: Joseph Cooper Date: Sat, 16 Sep 2023 19:01:22 +0100 Subject: [PATCH] Imrprove Firestore integration error handling (#206) Co-authored-by: Joseph Cooper --- .github/workflows/data-integration-tests.yml | 2 +- .../chartlist/FirebaseChartsRepository.kt | 50 ++++++--- .../maplibrarian/utils/LogErrorAndMap.kt | 5 +- .../chartlist/AddNewChartWorkflow.kt | 12 +- .../chartlist/ChartsRepository.kt | 29 +++-- .../maplibrarian/chartlist/ChartsWorkflow.kt | 5 +- .../chartlist/ChartsWorkflowTest.kt | 4 +- .../firebase/FirebaseEndToEndTest.kt | 55 +--------- .../maplibrarian/firebase/TestFixtures.kt | 50 +++++++-- .../auth/FirebaseUserRepositoryTest.kt | 103 +++++++----------- scripts/run-instrumented-integration-tests.sh | 3 + .../kotlin/com/omricat/logging/Loggable.kt | 2 +- 12 files changed, 154 insertions(+), 166 deletions(-) create mode 100755 scripts/run-instrumented-integration-tests.sh diff --git a/.github/workflows/data-integration-tests.yml b/.github/workflows/data-integration-tests.yml index 85acba0b..699547d5 100644 --- a/.github/workflows/data-integration-tests.yml +++ b/.github/workflows/data-integration-tests.yml @@ -112,7 +112,7 @@ jobs: adb uninstall "com.omricat.maplibrarian.integrationtesting.debug" || true adb uninstall "com.omricat.maplibrarian.debug" || true ./gradlew installDebug - ./scripts/capture-logcat-logs.sh adb shell am instrument -w com.omricat.maplibrarian.integrationtesting.debug/androidx.test.runner.AndroidJUnitRunner + ./scripts/capture-logcat-logs.sh ./scripts/run-instrumented-integration-tests.sh - name: Extract logs from firebase emulator container if: ${{ always() }} diff --git a/app/src/main/kotlin/com/omricat/maplibrarian/chartlist/FirebaseChartsRepository.kt b/app/src/main/kotlin/com/omricat/maplibrarian/chartlist/FirebaseChartsRepository.kt index 1bc2a37b..7db82847 100644 --- a/app/src/main/kotlin/com/omricat/maplibrarian/chartlist/FirebaseChartsRepository.kt +++ b/app/src/main/kotlin/com/omricat/maplibrarian/chartlist/FirebaseChartsRepository.kt @@ -10,11 +10,25 @@ import com.github.michaelbull.result.onFailure import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.FirebaseFirestoreException +import com.google.firebase.firestore.FirebaseFirestoreException.Code.ALREADY_EXISTS +import com.google.firebase.firestore.FirebaseFirestoreException.Code.CANCELLED +import com.google.firebase.firestore.FirebaseFirestoreException.Code.DATA_LOSS +import com.google.firebase.firestore.FirebaseFirestoreException.Code.INTERNAL +import com.google.firebase.firestore.FirebaseFirestoreException.Code.OK +import com.google.firebase.firestore.FirebaseFirestoreException.Code.UNAVAILABLE +import com.google.firebase.firestore.FirebaseFirestoreException.Code.UNKNOWN import com.google.firebase.firestore.QuerySnapshot import com.omricat.firebase.interop.runCatchingFirebaseException import com.omricat.logging.Loggable import com.omricat.logging.Logger import com.omricat.logging.log +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError.Cancelled +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError.ChartExists +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError.OtherException +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError.Unavailable +import com.omricat.maplibrarian.chartlist.ChartsRepository.Error.ExceptionWrappingError +import com.omricat.maplibrarian.chartlist.ChartsRepository.Error.MessageError import com.omricat.maplibrarian.model.ChartId import com.omricat.maplibrarian.model.DbChartModel import com.omricat.maplibrarian.model.DbChartModelFromMapDeserializer @@ -22,7 +36,7 @@ import com.omricat.maplibrarian.model.UnsavedChartModel import com.omricat.maplibrarian.model.User import com.omricat.maplibrarian.model.serializedToMap import com.omricat.maplibrarian.utils.DispatcherProvider -import com.omricat.maplibrarian.utils.logErrorAndMap +import com.omricat.maplibrarian.utils.logAndMapException import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext @@ -31,7 +45,6 @@ class FirebaseChartsRepository( private val dispatchers: DispatcherProvider = DispatcherProvider.Default, override val logger: Logger ) : ChartsRepository, Loggable { - override suspend fun chartsListForUser( user: User ): Result, ChartsRepository.Error> = @@ -40,19 +53,18 @@ class FirebaseChartsRepository( db.mapsCollection(user).get().await() } } - .mapError(ChartsServiceError::fromThrowable) - .onFailure { log(Warn) { "$it" } } + .logAndMapException(::ExceptionWrappingError) .andThen { snapshot -> snapshot .map { m -> m.parseMapModel() } .combine() - .mapError { e -> ChartsServiceError(e.message) } + .mapError { e -> MessageError(e.message) } } override suspend fun addNewChart( user: User, newChart: UnsavedChartModel - ): Result { + ): Result { require(user.id == newChart.userId) { "UserId of newMap (was ${newChart.userId}) must be " + "same as userId of user (was ${user.id})" @@ -62,7 +74,23 @@ class FirebaseChartsRepository( db.mapsCollection(user).add(newChart.serializedToMap()).await() } } - .logErrorAndMap(ChartsServiceError::fromThrowable) + .logAndMapException { exception -> + when (exception.code) { + UNAVAILABLE -> Unavailable + ALREADY_EXISTS -> ChartExists(newChart) + CANCELLED -> Cancelled + INTERNAL -> error("Firebase threw an internal error. This is unrecoverable.") + DATA_LOSS -> error("Firebase indicated unrecoverable data loss or corruption") + OK -> + error( + "FirebaseFirestoreException $exception had a status code of OK. " + + "Docs say this should never happen." + ) + UNKNOWN -> OtherException(exception) + else -> OtherException(exception) + } + } + .onFailure { log(Warn) { "error Adding New Chart: ${it.message}" } } .map { ref -> newChart.withChartId(ChartId(ref.id)) } } @@ -70,14 +98,6 @@ class FirebaseChartsRepository( collection("users").document(user.id.value).collection("maps") } -/* - It is always safe to upcast ChartModel to ChartModel since the only - possible value for a val of type Nothing? is null. - - It is safe to cast ChartModel to ChartModel immediately after setting - the chartId parameter to a non-null value. -*/ -@Suppress("UNCHECKED_CAST") private fun UnsavedChartModel.withChartId(chartId: ChartId): DbChartModel = DbChartModel(userId = userId, title = title, chartId = chartId) diff --git a/app/src/main/kotlin/com/omricat/maplibrarian/utils/LogErrorAndMap.kt b/app/src/main/kotlin/com/omricat/maplibrarian/utils/LogErrorAndMap.kt index 4170966c..014e473e 100644 --- a/app/src/main/kotlin/com/omricat/maplibrarian/utils/LogErrorAndMap.kt +++ b/app/src/main/kotlin/com/omricat/maplibrarian/utils/LogErrorAndMap.kt @@ -9,5 +9,6 @@ import com.omricat.logging.log context(Loggable) -inline fun Result.logErrorAndMap(transform: (Throwable) -> E): Result = - this.onFailure { logger.log(Warn, throwable = it) { "" } }.mapError(transform) +inline fun Result.logAndMapException( + transform: (T) -> E +): Result = this.onFailure { logger.log(Warn, throwable = it) { "" } }.mapError(transform) diff --git a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/AddNewChartWorkflow.kt b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/AddNewChartWorkflow.kt index 9f2c1796..4edaa88e 100644 --- a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/AddNewChartWorkflow.kt +++ b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/AddNewChartWorkflow.kt @@ -9,6 +9,7 @@ import com.omricat.maplibrarian.chartlist.AddNewChartWorkflow.Event.Saved import com.omricat.maplibrarian.chartlist.AddNewChartWorkflow.State import com.omricat.maplibrarian.chartlist.AddNewChartWorkflow.State.Editing import com.omricat.maplibrarian.chartlist.AddNewChartWorkflow.State.Saving +import com.omricat.maplibrarian.chartlist.ChartsRepository.AddNewChartError import com.omricat.maplibrarian.model.ChartModel import com.omricat.maplibrarian.model.DbChartModel import com.omricat.maplibrarian.model.UnsavedChartModel @@ -77,17 +78,18 @@ public class AddNewChartWorkflow(private val chartsRepository: ChartsRepository) override fun snapshotState(state: State): Snapshot = state.toSnapshot() - internal fun onErrorSaving(chart: UnsavedChartModel, e: ChartsRepository.Error) = action { - state = Editing(chart, errorMessage = e.message) - } + internal fun onErrorSaving(chart: UnsavedChartModel, e: ChartsRepository.AddNewChartError) = + action { + state = Editing(chart, errorMessage = e.message) + } internal fun onNewItemSaved(savedChart: DbChartModel) = action { setOutput(Saved) } private fun saveNewItem( user: User, chart: UnsavedChartModel - ): Worker> = - resultWorker(ChartsServiceError::fromThrowable) { + ): Worker> = + resultWorker({ e -> AddNewChartError.OtherException(e) }) { chartsRepository.addNewChart(user, chart) } diff --git a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsRepository.kt b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsRepository.kt index cbdfa226..09382ba2 100644 --- a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsRepository.kt +++ b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsRepository.kt @@ -4,7 +4,6 @@ import com.github.michaelbull.result.Result import com.omricat.maplibrarian.model.DbChartModel import com.omricat.maplibrarian.model.UnsavedChartModel import com.omricat.maplibrarian.model.User -import kotlinx.serialization.Serializable public interface ChartsRepository { public suspend fun chartsListForUser( @@ -14,20 +13,28 @@ public interface ChartsRepository { public suspend fun addNewChart( user: User, newChart: UnsavedChartModel - ): Result + ): Result - public sealed interface Error { + public interface Error { public val message: String + + public data class MessageError(override val message: String) : Error + + public data class ExceptionWrappingError(public val exception: Throwable) : Error { + override val message: String + get() = exception.message ?: "No message in exception $exception" + } } -} -// public typealias ChartsServiceError = ChartsService.Error -@Serializable -public data class ChartsServiceError(override val message: String) : ChartsRepository.Error { - private constructor(throwable: Throwable) : this(throwable.message ?: "Unknown error") + public sealed class AddNewChartError(public val message: String) { + public data object Unavailable : AddNewChartError("Service temporarily unavailable") + + public data object Cancelled : AddNewChartError("Operation ") + + public data class ChartExists(public val unsavedChartModel: UnsavedChartModel) : + AddNewChartError("Chart already exists: $unsavedChartModel") - public companion object { - public fun fromThrowable(throwable: Throwable): ChartsServiceError = - ChartsServiceError(throwable) + public data class OtherException(val exception: Throwable) : + AddNewChartError(exception.message ?: "No message in $exception") } } diff --git a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflow.kt b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflow.kt index e223754d..0dd4a77b 100644 --- a/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflow.kt +++ b/core/src/main/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflow.kt @@ -5,6 +5,7 @@ import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.map import com.omricat.maplibrarian.chartlist.ActualChartsWorkflow.Props import com.omricat.maplibrarian.chartlist.ChartsListWorkflow.Event.SelectItem +import com.omricat.maplibrarian.chartlist.ChartsRepository.Error.ExceptionWrappingError import com.omricat.maplibrarian.chartlist.ChartsScreen.Loading import com.omricat.maplibrarian.chartlist.ChartsScreen.ShowError import com.omricat.maplibrarian.chartlist.ChartsWorkflowState.AddingItem @@ -85,9 +86,7 @@ public class ActualChartsWorkflow( chartsRepository: ChartsRepository, user: User ): Worker, ChartsRepository.Error>> = - resultWorker(ChartsServiceError::fromThrowable) { - chartsRepository.chartsListForUser(user) - } + resultWorker(::ExceptionWrappingError) { chartsRepository.chartsListForUser(user) } } } diff --git a/core/src/test/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflowTest.kt b/core/src/test/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflowTest.kt index 3905100e..2dd66e1d 100644 --- a/core/src/test/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflowTest.kt +++ b/core/src/test/kotlin/com/omricat/maplibrarian/chartlist/ChartsWorkflowTest.kt @@ -33,7 +33,9 @@ internal class ChartsWorkflowTest : "transform ErrorLoadingCharts to RequestData" { val state = - ChartsWorkflowState.ErrorLoadingCharts(ChartsServiceError("Error message")) + ChartsWorkflowState.ErrorLoadingCharts( + ChartsRepository.Error.MessageError("Error message") + ) ChartsWorkflowState.fromSnapshot(state.toSnapshot()) shouldBe ChartsWorkflowState.RequestData diff --git a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/FirebaseEndToEndTest.kt b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/FirebaseEndToEndTest.kt index 47e13929..e24c3f92 100644 --- a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/FirebaseEndToEndTest.kt +++ b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/FirebaseEndToEndTest.kt @@ -1,27 +1,25 @@ package com.omricat.maplibrarian.firebase -import android.annotation.SuppressLint import assertk.all import assertk.assertThat import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.prop import com.github.michaelbull.result.getOrThrow -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.firestore.FirebaseFirestore import com.omricat.logging.test.TestLogger import com.omricat.maplibrarian.auth.EmailPasswordCredential import com.omricat.maplibrarian.auth.FirebaseUserRepository import com.omricat.maplibrarian.chartlist.FirebaseChartsRepository -import com.omricat.maplibrarian.firebase.auth.FirebaseAuthEmulatorRestApi -import com.omricat.maplibrarian.firebase.charts.FirebaseFirestoreRestApi +import com.omricat.maplibrarian.firebase.TestFixtures.authApi +import com.omricat.maplibrarian.firebase.TestFixtures.firebaseAuthInstance +import com.omricat.maplibrarian.firebase.TestFixtures.firestoreApi +import com.omricat.maplibrarian.firebase.TestFixtures.firestoreInstance import com.omricat.maplibrarian.model.UnsavedChartModel import com.omricat.result.assertk.isOk import kotlin.time.ExperimentalTime import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.BeforeClass import org.junit.Test @Suppress("FunctionName") @@ -65,49 +63,4 @@ class FirebaseEndToEndTest { prop("First item title") { it.first().title }.isEqualTo("New map") } } - - companion object Fixtures { - - // Not a problem to leak a Context in an instrumented test - @SuppressLint("StaticFieldLeak") - @JvmStatic - lateinit var firestoreInstance: FirebaseFirestore - - @JvmStatic lateinit var firestoreApi: FirebaseFirestoreRestApi - - @JvmStatic lateinit var firebaseAuthInstance: FirebaseAuth - - @JvmStatic lateinit var authApi: FirebaseAuthEmulatorRestApi - - @JvmStatic - @BeforeClass - fun setup() { - firestoreInstance = - FirebaseFirestore.getInstance(TestFixtures.app).apply { - useEmulator( - FirebaseEmulatorConnection.HOST, - FirebaseEmulatorConnection.FIRESTORE_PORT - ) - } - - firestoreApi = - FirebaseFirestoreRestApi( - TestFixtures.projectId, - TestFixtures.emulatorBaseUrl(FirebaseEmulatorConnection.FIRESTORE_PORT) - ) - - firebaseAuthInstance = - FirebaseAuth.getInstance(TestFixtures.app).apply { - useEmulator( - FirebaseEmulatorConnection.HOST, - FirebaseEmulatorConnection.AUTH_PORT - ) - } - authApi = - FirebaseAuthEmulatorRestApi( - TestFixtures.projectId, - TestFixtures.emulatorBaseUrl(FirebaseEmulatorConnection.AUTH_PORT) - ) - } - } } diff --git a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/TestFixtures.kt b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/TestFixtures.kt index 2bf9a237..eb866d9c 100644 --- a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/TestFixtures.kt +++ b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/TestFixtures.kt @@ -2,8 +2,11 @@ package com.omricat.maplibrarian.firebase import androidx.test.core.app.ApplicationProvider import com.google.firebase.FirebaseApp +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.firestore.FirebaseFirestore +import com.omricat.maplibrarian.firebase.auth.FirebaseAuthEmulatorRestApi +import com.omricat.maplibrarian.firebase.charts.FirebaseFirestoreRestApi import java.io.IOException -import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.SECONDS import kotlin.time.Duration import kotlin.time.ExperimentalTime @@ -20,20 +23,49 @@ private const val OKHTTP_CALL_TIMEOUT = 20L private const val OKHTTP_READ_TIMEOUT = 15L +@OptIn(ExperimentalTime::class) object TestFixtures { - val app: FirebaseApp by lazy { + private val app: FirebaseApp by lazy { FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext()) ?: error("Failed to initialize FirebaseApp") } - val projectId: String + private val projectId: String get() = app.options.projectId ?: error("Can't get projectId from FirebaseApp options") - fun emulatorBaseUrl(port: Int): HttpUrl = + val firestoreInstance: FirebaseFirestore by lazy { + FirebaseFirestore.getInstance(app).apply { + this.useEmulator( + FirebaseEmulatorConnection.HOST, + FirebaseEmulatorConnection.FIRESTORE_PORT + ) + } + } + + val firestoreApi: FirebaseFirestoreRestApi by lazy { + FirebaseFirestoreRestApi( + projectId, + emulatorBaseUrl(FirebaseEmulatorConnection.FIRESTORE_PORT) + ) + } + + val firebaseAuthInstance: FirebaseAuth by lazy { + FirebaseAuth.getInstance(app).apply { + useEmulator(FirebaseEmulatorConnection.HOST, FirebaseEmulatorConnection.AUTH_PORT) + } + } + + val authApi: FirebaseAuthEmulatorRestApi by lazy { + FirebaseAuthEmulatorRestApi( + projectId, + emulatorBaseUrl(FirebaseEmulatorConnection.AUTH_PORT) + ) + } + + private fun emulatorBaseUrl(port: Int): HttpUrl = Builder().host(FirebaseEmulatorConnection.HOST).port(port).scheme("http").build() - @OptIn(ExperimentalTime::class) - class OkHttpTimingEventListener( + internal class OkHttpTimingEventListener( private val timeSource: TimeSource, private val output: (String) -> Unit, ) : EventListener() { @@ -56,9 +88,6 @@ object TestFixtures { ) } - private fun log(call: Call, data: String) = - "[${TimeSource.Monotonic.markNow()}]: ${call.request().url} | $data" - override fun responseBodyEnd(call: Call, byteCount: Long) { readTime = readingStartTimestamp.elapsedNow() output("responseBodyEnd(): $call, ${call.request()}, read time: $readTime") @@ -69,11 +98,10 @@ object TestFixtures { } } - @OptIn(ExperimentalTime::class) fun okHttpClient(output: (String) -> Unit, timeSource: TimeSource = TimeSource.Monotonic) = OkHttpClient.Builder() .callTimeout(OKHTTP_CALL_TIMEOUT, SECONDS) - .readTimeout(OKHTTP_READ_TIMEOUT, TimeUnit.SECONDS) + .readTimeout(OKHTTP_READ_TIMEOUT, SECONDS) .eventListener(OkHttpTimingEventListener(output = output, timeSource = timeSource)) .build() } diff --git a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/auth/FirebaseUserRepositoryTest.kt b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/auth/FirebaseUserRepositoryTest.kt index 8b40e244..dd6745d4 100644 --- a/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/auth/FirebaseUserRepositoryTest.kt +++ b/integration-tests/firebase/src/main/kotlin/com/omricat/maplibrarian/firebase/auth/FirebaseUserRepositoryTest.kt @@ -5,23 +5,22 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.prop -import com.google.firebase.auth.FirebaseAuth import com.omricat.maplibrarian.auth.CreateUserError import com.omricat.maplibrarian.auth.EmailPasswordCredential import com.omricat.maplibrarian.auth.FirebaseUserRepository -import com.omricat.maplibrarian.firebase.FirebaseEmulatorConnection import com.omricat.maplibrarian.firebase.TestDispatcherProvider -import com.omricat.maplibrarian.firebase.TestFixtures +import com.omricat.maplibrarian.firebase.TestFixtures.authApi +import com.omricat.maplibrarian.firebase.TestFixtures.firebaseAuthInstance import com.omricat.maplibrarian.firebase.auth.FirebaseAuthEmulatorRestApi.TestUser import com.omricat.result.assertk.isErr import com.omricat.result.assertk.isOk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.BeforeClass import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(Enclosed::class) class FirebaseUserRepositoryTest { @Before @@ -31,39 +30,37 @@ class FirebaseUserRepositoryTest { private val testCredential = EmailPasswordCredential("test@example.com", "password") - class CreateUserTest { - @Test - fun addUserSucceeds() = runTest { - val repository = - FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) - val createUserResult = - repository.createUser(EmailPasswordCredential("test@example.com", "password")) - assertThat(createUserResult).isOk() - } + @Test + fun addUserSucceeds() = runTest { + val repository = + FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) + val createUserResult = + repository.createUser(EmailPasswordCredential("test@example.com", "password")) + assertThat(createUserResult).isOk() + } - @Test - fun tooShortPasswordGivesError() = runTest { - val repository = - FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) - val createUserResult = - repository.createUser(EmailPasswordCredential("test@example.com", "pw")) - assertThat(createUserResult).isErr().isInstanceOf() - } + @Test + fun tooShortPasswordGivesError() = runTest { + val repository = + FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) + val createUserResult = + repository.createUser(EmailPasswordCredential("test@example.com", "pw")) + assertThat(createUserResult).isErr().isInstanceOf() + } - @Test - fun emailCollisionGivesError() = runTest { - val repository = - FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) - val firstCreateUserResult = - repository.createUser(EmailPasswordCredential("test@example.com", "password")) - val secondCreateUserResult = - repository.createUser(EmailPasswordCredential("test@example.com", "password")) - assertAll { - assertThat(firstCreateUserResult).isOk() - assertThat(secondCreateUserResult) - .isErr() - .isInstanceOf() - } + @Test + fun emailCollisionGivesError() = runTest { + val repository = + FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) + val firstCreateUserResult = + repository.createUser(EmailPasswordCredential("test@example.com", "password")) + val secondCreateUserResult = + repository.createUser(EmailPasswordCredential("test@example.com", "password")) + assertAll { + assertThat(firstCreateUserResult).isOk() + assertThat(secondCreateUserResult) + .isErr() + .isInstanceOf() } } @@ -79,7 +76,7 @@ class FirebaseUserRepositoryTest { @Test fun canSignInExternallyAddedUser() = runTest { - val createdUser = Fixtures.createUserViaRestApi(testCredential) + val createdUser = createUserViaRestApi(testCredential) val repository = FirebaseUserRepository(firebaseAuthInstance, TestDispatcherProvider(testScheduler)) val signInResult = repository.attemptAuthentication(testCredential) @@ -89,31 +86,7 @@ class FirebaseUserRepositoryTest { .isEqualTo(createdUser.email) } - companion object Fixtures { - - @JvmStatic lateinit var firebaseAuthInstance: FirebaseAuth - - @JvmStatic lateinit var authApi: FirebaseAuthEmulatorRestApi - - @JvmStatic - @BeforeClass - fun setUp() { - firebaseAuthInstance = - FirebaseAuth.getInstance(TestFixtures.app).apply { - useEmulator( - FirebaseEmulatorConnection.HOST, - FirebaseEmulatorConnection.AUTH_PORT - ) - } - authApi = - FirebaseAuthEmulatorRestApi( - TestFixtures.projectId, - TestFixtures.emulatorBaseUrl(FirebaseEmulatorConnection.AUTH_PORT) - ) - } - - fun createUserViaRestApi(credential: EmailPasswordCredential): TestUser = - authApi.createUser(credential).body() - ?: error("Failed to create user with credentials $credential") - } + private fun createUserViaRestApi(credential: EmailPasswordCredential): TestUser = + authApi.createUser(credential).body() + ?: error("Failed to create user with credentials $credential") } diff --git a/scripts/run-instrumented-integration-tests.sh b/scripts/run-instrumented-integration-tests.sh new file mode 100755 index 00000000..9df8df37 --- /dev/null +++ b/scripts/run-instrumented-integration-tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -o errexit -o nounset +adb shell am instrument -w com.omricat.maplibrarian.integrationtesting.debug/androidx.test.runner.AndroidJUnitRunner diff --git a/util/logging/src/main/kotlin/com/omricat/logging/Loggable.kt b/util/logging/src/main/kotlin/com/omricat/logging/Loggable.kt index 312b09c7..a9c5e4b1 100644 --- a/util/logging/src/main/kotlin/com/omricat/logging/Loggable.kt +++ b/util/logging/src/main/kotlin/com/omricat/logging/Loggable.kt @@ -25,7 +25,7 @@ public inline fun T.log( priority: Severity = Debug, tag: String? = null, throwable: Throwable, - noinline message: () -> String + noinline message: () -> String = { throwable.message ?: "$throwable" } ) { logger.log(priority, Tag(tag ?: T::class.outerClassSimpleName()), throwable, message) }