From bd5167f625f573747424d56af920f446149e4147 Mon Sep 17 00:00:00 2001 From: Kayla Brady <31781298+KaylaBrady@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:45:34 -0400 Subject: [PATCH] feat(ConfigUseCase): log AppCheck failures (#466) * feat(ConfigUseCase): log AppCheck failures * test(ConfigUseCase): Add test that sentry is called * fix: wire up sentry repo * fix(ConfigUseCase): use sentry repo for captureException * Update shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt Co-authored-by: Melody Horn * style(RepositoryDI): alphabetize * fix(E2ERepos): add sentry imports * fix(ContentViewModelTests): Mock sentry repo --------- Co-authored-by: Melody Horn --- .../Views/ContentViewModelTests.swift | 3 +- .../mbta_app/dependencyInjection/AppModule.kt | 4 +- .../dependencyInjection/RepositoryDI.kt | 8 ++++ .../mbta_app/endToEnd/EndToEndRepositories.kt | 5 ++- .../mbta_app/repositories/SentryRepository.kt | 30 +++++++++++++++ .../tid/mbta_app/usecases/ConfigUseCase.kt | 11 +++++- .../mbta_app/usecases/ConfigUsecaseTests.kt | 37 +++++++++++++++++-- 7 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SentryRepository.kt diff --git a/iosApp/iosAppTests/Views/ContentViewModelTests.swift b/iosApp/iosAppTests/Views/ContentViewModelTests.swift index c54309d9b..e96d1494e 100644 --- a/iosApp/iosAppTests/Views/ContentViewModelTests.swift +++ b/iosApp/iosAppTests/Views/ContentViewModelTests.swift @@ -19,7 +19,8 @@ final class ContentViewModelTests: XCTestCase { let expectedResult = ApiResultOk(data: .init(mapboxPublicToken: "FAKE_TOKEN")) let contentVM = ContentViewModel(configUseCase: ConfigUseCase( appCheckRepo: MockAppCheckRepository(), - configRepo: MockConfigRepository(response: expectedResult) + configRepo: MockConfigRepository(response: expectedResult), + sentryRepo: MockSentryRepository() )) await contentVM.loadConfig() XCTAssertEqual(contentVM.configResponse, expectedResult) diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/AppModule.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/AppModule.kt index a39e14584..f1748c8eb 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/AppModule.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/AppModule.kt @@ -15,6 +15,7 @@ import com.mbta.tid.mbta_app.repositories.IPredictionsRepository import com.mbta.tid.mbta_app.repositories.IRailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.ISchedulesRepository import com.mbta.tid.mbta_app.repositories.ISearchResultRepository +import com.mbta.tid.mbta_app.repositories.ISentryRepository import com.mbta.tid.mbta_app.repositories.ISettingsRepository import com.mbta.tid.mbta_app.repositories.IStopRepository import com.mbta.tid.mbta_app.repositories.ITripPredictionsRepository @@ -50,6 +51,7 @@ fun repositoriesModule(repositories: IRepositories): Module { single { repositories.railRouteShapes } single { repositories.schedules } single { repositories.searchResults } + single { repositories.sentry } single { repositories.settings } single { repositories.stop } single { repositories.trip } @@ -64,7 +66,7 @@ fun repositoriesModule(repositories: IRepositories): Module { repositories.vehicle?.let { vehicleRepo -> factory { vehicleRepo } } repositories.vehicles?.let { vehiclesRepo -> factory { vehiclesRepo } } single { repositories.visitHistory } - single { ConfigUseCase(get(), get()) } + single { ConfigUseCase(get(), get(), get()) } single { GetSettingUsecase(get()) } single { TogglePinnedRouteUsecase(get()) } single { VisitHistoryUsecase(get()) } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt index 5f96a0cf5..067675525 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/dependencyInjection/RepositoryDI.kt @@ -13,6 +13,7 @@ import com.mbta.tid.mbta_app.repositories.IPredictionsRepository import com.mbta.tid.mbta_app.repositories.IRailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.ISchedulesRepository import com.mbta.tid.mbta_app.repositories.ISearchResultRepository +import com.mbta.tid.mbta_app.repositories.ISentryRepository import com.mbta.tid.mbta_app.repositories.ISettingsRepository import com.mbta.tid.mbta_app.repositories.IStopRepository import com.mbta.tid.mbta_app.repositories.ITripPredictionsRepository @@ -32,6 +33,7 @@ import com.mbta.tid.mbta_app.repositories.MockAppCheckRepository import com.mbta.tid.mbta_app.repositories.MockConfigRepository import com.mbta.tid.mbta_app.repositories.MockErrorBannerStateRepository import com.mbta.tid.mbta_app.repositories.MockPredictionsRepository +import com.mbta.tid.mbta_app.repositories.MockSentryRepository import com.mbta.tid.mbta_app.repositories.MockSettingsRepository import com.mbta.tid.mbta_app.repositories.MockTripPredictionsRepository import com.mbta.tid.mbta_app.repositories.MockVehicleRepository @@ -41,6 +43,7 @@ import com.mbta.tid.mbta_app.repositories.PinnedRoutesRepository import com.mbta.tid.mbta_app.repositories.RailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.SchedulesRepository import com.mbta.tid.mbta_app.repositories.SearchResultRepository +import com.mbta.tid.mbta_app.repositories.SentryRepository import com.mbta.tid.mbta_app.repositories.SettingsRepository import com.mbta.tid.mbta_app.repositories.StopRepository import com.mbta.tid.mbta_app.repositories.TripRepository @@ -60,6 +63,7 @@ interface IRepositories { val railRouteShapes: IRailRouteShapeRepository val schedules: ISchedulesRepository val searchResults: ISearchResultRepository + val sentry: ISentryRepository val settings: ISettingsRepository val stop: IStopRepository val trip: ITripRepository @@ -81,6 +85,7 @@ class RepositoryDI : IRepositories, KoinComponent { override val railRouteShapes: IRailRouteShapeRepository by inject() override val schedules: ISchedulesRepository by inject() override val searchResults: ISearchResultRepository by inject() + override val sentry: ISentryRepository by inject() override val settings: ISettingsRepository by inject() override val stop: IStopRepository by inject() override val trip: ITripRepository by inject() @@ -105,6 +110,7 @@ class RealRepositories : IRepositories { override val railRouteShapes = RailRouteShapeRepository() override val schedules = SchedulesRepository() override val searchResults = SearchResultRepository() + override val sentry = SentryRepository() override val settings = SettingsRepository() override val stop = StopRepository() override val trip = TripRepository() @@ -126,6 +132,7 @@ class MockRepositories( override val railRouteShapes: IRailRouteShapeRepository, override val schedules: ISchedulesRepository, override val searchResults: ISearchResultRepository, + override val sentry: ISentryRepository, override val settings: ISettingsRepository, override val stop: IStopRepository, override val trip: ITripRepository, @@ -155,6 +162,7 @@ class MockRepositories( railRouteShapes = IdleRailRouteShapeRepository(), schedules = schedules, searchResults = IdleSearchResultRepository(), + sentry = MockSentryRepository(), settings = MockSettingsRepository(), stop = stop, trip = trip, diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt index f67248fae..76aa140a4 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/endToEnd/EndToEndRepositories.kt @@ -29,6 +29,7 @@ import com.mbta.tid.mbta_app.repositories.IPredictionsRepository import com.mbta.tid.mbta_app.repositories.IRailRouteShapeRepository import com.mbta.tid.mbta_app.repositories.ISchedulesRepository import com.mbta.tid.mbta_app.repositories.ISearchResultRepository +import com.mbta.tid.mbta_app.repositories.ISentryRepository import com.mbta.tid.mbta_app.repositories.ISettingsRepository import com.mbta.tid.mbta_app.repositories.IStopRepository import com.mbta.tid.mbta_app.repositories.ITripPredictionsRepository @@ -42,6 +43,7 @@ import com.mbta.tid.mbta_app.repositories.MockAppCheckRepository import com.mbta.tid.mbta_app.repositories.MockConfigRepository import com.mbta.tid.mbta_app.repositories.MockErrorBannerStateRepository import com.mbta.tid.mbta_app.repositories.MockSearchResultRepository +import com.mbta.tid.mbta_app.repositories.MockSentryRepository import com.mbta.tid.mbta_app.repositories.MockSettingsRepository import com.mbta.tid.mbta_app.repositories.MockVehiclesRepository import com.mbta.tid.mbta_app.repositories.MockVisitHistoryRepository @@ -171,6 +173,7 @@ fun endToEndModule(): Module { } } single { MockSearchResultRepository() } + single { MockSentryRepository() } single { MockSettingsRepository() } single { object : IStopRepository { @@ -229,7 +232,7 @@ fun endToEndModule(): Module { } single { MockVehiclesRepository() } single { MockVisitHistoryRepository() } - single { ConfigUseCase(get(), get()) } + single { ConfigUseCase(get(), get(), get()) } single { GetSettingUsecase(get()) } single { TogglePinnedRouteUsecase(get()) } single { VisitHistoryUsecase(get()) } diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SentryRepository.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SentryRepository.kt new file mode 100644 index 000000000..b0fd554a2 --- /dev/null +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/repositories/SentryRepository.kt @@ -0,0 +1,30 @@ +package com.mbta.tid.mbta_app.repositories + +import io.sentry.kotlin.multiplatform.Sentry + +interface ISentryRepository { + + fun captureMessage(msg: String) + + fun captureException(throwable: Throwable) +} + +class SentryRepository : ISentryRepository { + override fun captureMessage(msg: String) { + Sentry.captureMessage(msg) + } + + override fun captureException(throwable: Throwable) { + Sentry.captureException(throwable) + } +} + +class MockSentryRepository : ISentryRepository { + override fun captureMessage(msg: String) { + TODO("Not yet implemented") + } + + override fun captureException(throwable: Throwable) { + TODO("Not yet implemented") + } +} diff --git a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUseCase.kt b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUseCase.kt index 3207835de..2ad5dbb64 100644 --- a/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUseCase.kt +++ b/shared/src/commonMain/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUseCase.kt @@ -4,23 +4,30 @@ import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.model.response.ConfigResponse import com.mbta.tid.mbta_app.repositories.IAppCheckRepository import com.mbta.tid.mbta_app.repositories.IConfigRepository +import com.mbta.tid.mbta_app.repositories.ISentryRepository import org.koin.core.component.KoinComponent class ConfigUseCase( private val appCheckRepo: IAppCheckRepository, - private val configRepo: IConfigRepository + private val configRepo: IConfigRepository, + private val sentryRepo: ISentryRepository ) : KoinComponent { suspend fun getConfig(): ApiResult = try { when (val tokenResult = appCheckRepo.getToken()) { - is ApiResult.Error -> + is ApiResult.Error -> { + sentryRepo.captureMessage( + "AppCheck token error ${tokenResult.code} ${tokenResult.message}" + ) ApiResult.Error(message = "app check token failure ${tokenResult.message}") + } is ApiResult.Ok -> { configRepo.getConfig(tokenResult.data.token) } } } catch (e: Exception) { + sentryRepo.captureException(e) ApiResult.Error(message = e.message ?: e.toString()) } } diff --git a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUsecaseTests.kt b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUsecaseTests.kt index f7b21f574..173031955 100644 --- a/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUsecaseTests.kt +++ b/shared/src/commonTest/kotlin/com/mbta/tid/mbta_app/usecases/ConfigUsecaseTests.kt @@ -2,8 +2,16 @@ package com.mbta.tid.mbta_app.usecases import com.mbta.tid.mbta_app.model.response.ApiResult import com.mbta.tid.mbta_app.model.response.ConfigResponse +import com.mbta.tid.mbta_app.repositories.IAppCheckRepository +import com.mbta.tid.mbta_app.repositories.ISentryRepository import com.mbta.tid.mbta_app.repositories.MockAppCheckRepository import com.mbta.tid.mbta_app.repositories.MockConfigRepository +import com.mbta.tid.mbta_app.repositories.MockSentryRepository +import dev.mokkery.MockMode +import dev.mokkery.answering.throws +import dev.mokkery.everySuspend +import dev.mokkery.mock +import dev.mokkery.verify import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.coroutines.runBlocking @@ -15,10 +23,10 @@ class ConfigUsecaseTests { val appCheckRepo = MockAppCheckRepository() val configRepo = MockConfigRepository(ApiResult.Ok(ConfigResponse(mapboxPublicToken = "fake_token"))) + val mockSentryRepo = MockSentryRepository() runBlocking { - val response = - ConfigUseCase(appCheckRepo = appCheckRepo, configRepo = configRepo).getConfig() + val response = ConfigUseCase(appCheckRepo, configRepo, mockSentryRepo).getConfig() assertEquals(response, ApiResult.Ok(ConfigResponse("fake_token"))) } @@ -29,12 +37,33 @@ class ConfigUsecaseTests { val appCheckRepo = MockAppCheckRepository(ApiResult.Error(message = "oops")) val configRepo = MockConfigRepository(ApiResult.Ok(ConfigResponse(mapboxPublicToken = "fake_token"))) + val mockSentryRepo = mock(MockMode.autofill) runBlocking { - val response = - ConfigUseCase(appCheckRepo = appCheckRepo, configRepo = configRepo).getConfig() + val response = ConfigUseCase(appCheckRepo, configRepo, mockSentryRepo).getConfig() assertEquals(ApiResult.Error(message = "app check token failure oops"), response) + + verify { mockSentryRepo.captureMessage("AppCheck token error null oops") } + } + } + + @Test + fun testGetConfigAppCheckException() { + val appCheckRepo = mock(MockMode.autofill) + val expectedException = IllegalArgumentException("oops") + + everySuspend { appCheckRepo.getToken() } throws expectedException + val configRepo = + MockConfigRepository(ApiResult.Ok(ConfigResponse(mapboxPublicToken = "fake_token"))) + val mockSentryRepo = mock(MockMode.autofill) + + runBlocking { + val response = ConfigUseCase(appCheckRepo, configRepo, mockSentryRepo).getConfig() + + assertEquals(ApiResult.Error(message = "oops"), response) + + verify { mockSentryRepo.captureException(expectedException) } } } }