Loading app/src/main/java/foundation/e/apps/feature/auth/source/UpdateSourceSelectionCoordinator.kt +17 −5 Original line number Diff line number Diff line Loading @@ -70,24 +70,35 @@ class UpdateSourceSelectionCoordinator @Inject constructor( previousSelection: SourceSelection, originalFailure: Exception, ): Nothing { rollbackSourceSelection(previousSelection, originalFailure) val restoredSelection = rollbackSourceSelection( requestedSelection = requestedSelection, previousSelection = previousSelection, originalFailure = originalFailure, ) throw SourceSelectionUpdateException( requestedSelection = requestedSelection, restoredSelection = previousSelection, restoredSelection = restoredSelection, cause = originalFailure, ) } private suspend fun rollbackSourceSelection( requestedSelection: SourceSelection, previousSelection: SourceSelection, originalFailure: Exception, ) { try { ): SourceSelection { // The new selection was already persisted before this rollback ran (otherwise // applySourceSelectionSideEffects would not have been entered), so a failure to // re-write the previous selection leaves the requested selection persisted. // restoredSelection must reflect what is actually persisted on disk so callers do not // misreport the post-failure state to the user. val restoredSelection = try { sourceSelectionRepository.saveSourceSelection(previousSelection) previousSelection } catch (exception: IllegalStateException) { originalFailure.addSuppressed(exception) Timber.w(exception, "Failed to restore previous source selection") return return requestedSelection } runRollbackStep( Loading @@ -105,6 +116,7 @@ class UpdateSourceSelectionCoordinator @Inject constructor( sessionStateController.clearLoadedSessions() sessionStateController.refreshSessions() } return restoredSelection } private suspend fun runRollbackStep( Loading app/src/test/java/foundation/e/apps/feature/auth/source/UpdateSourceSelectionCoordinatorTest.kt +36 −0 Original line number Diff line number Diff line Loading @@ -136,6 +136,38 @@ class UpdateSourceSelectionCoordinatorTest { } } @Test fun `invoke reports requested selection as restored when rollback save fails`() = runTest { val pendingUpdatesRepository = FakePendingUpdatesRepository() val sourceSelectionRepository = FakeSourceSelectionRepository( initialSourceSelection = SourceSelection.DEFAULT, saveFailureOnSelection = SourceSelection.DEFAULT, ) val periodicUpdatesScheduler = mockk<PeriodicUpdatesScheduler>() val sessionStateController = FakeSessionStateController() val coordinator = UpdateSourceSelectionCoordinator( pendingUpdatesRepository = pendingUpdatesRepository, periodicUpdatesScheduler = periodicUpdatesScheduler, sessionStateController = sessionStateController, sourceSelectionRepository = sourceSelectionRepository, ) val updatedSelection = SourceSelection.OPEN_SOURCE_AND_PWA coEvery { periodicUpdatesScheduler.syncPeriodicUpdates( existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, ) } throws IOException("schedule failed") val exception = kotlin.test.assertFailsWith<SourceSelectionUpdateException> { coordinator(updatedSelection) } assertThat(exception.requestedSelection).isEqualTo(updatedSelection) assertThat(exception.restoredSelection).isEqualTo(updatedSelection) assertThat(sourceSelectionRepository.currentSourceSelection()).isEqualTo(updatedSelection) assertThat(exception.cause?.suppressed?.toList()).hasSize(1) } private class FakePendingUpdatesRepository : PendingUpdatesRepository { var clearPendingUpdatesCallCount = 0 Loading @@ -146,6 +178,7 @@ class UpdateSourceSelectionCoordinatorTest { private class FakeSourceSelectionRepository( initialSourceSelection: SourceSelection, private val saveFailureOnSelection: SourceSelection? = null, ) : SourceSelectionRepository { private val sourceSelectionState = MutableStateFlow(initialSourceSelection) Loading @@ -154,6 +187,9 @@ class UpdateSourceSelectionCoordinatorTest { override fun currentSourceSelection(): SourceSelection = sourceSelectionState.value override fun saveSourceSelection(sourceSelection: SourceSelection) { if (sourceSelection == saveFailureOnSelection) { throw IllegalStateException("save failed") } sourceSelectionState.value = sourceSelection } } Loading Loading
app/src/main/java/foundation/e/apps/feature/auth/source/UpdateSourceSelectionCoordinator.kt +17 −5 Original line number Diff line number Diff line Loading @@ -70,24 +70,35 @@ class UpdateSourceSelectionCoordinator @Inject constructor( previousSelection: SourceSelection, originalFailure: Exception, ): Nothing { rollbackSourceSelection(previousSelection, originalFailure) val restoredSelection = rollbackSourceSelection( requestedSelection = requestedSelection, previousSelection = previousSelection, originalFailure = originalFailure, ) throw SourceSelectionUpdateException( requestedSelection = requestedSelection, restoredSelection = previousSelection, restoredSelection = restoredSelection, cause = originalFailure, ) } private suspend fun rollbackSourceSelection( requestedSelection: SourceSelection, previousSelection: SourceSelection, originalFailure: Exception, ) { try { ): SourceSelection { // The new selection was already persisted before this rollback ran (otherwise // applySourceSelectionSideEffects would not have been entered), so a failure to // re-write the previous selection leaves the requested selection persisted. // restoredSelection must reflect what is actually persisted on disk so callers do not // misreport the post-failure state to the user. val restoredSelection = try { sourceSelectionRepository.saveSourceSelection(previousSelection) previousSelection } catch (exception: IllegalStateException) { originalFailure.addSuppressed(exception) Timber.w(exception, "Failed to restore previous source selection") return return requestedSelection } runRollbackStep( Loading @@ -105,6 +116,7 @@ class UpdateSourceSelectionCoordinator @Inject constructor( sessionStateController.clearLoadedSessions() sessionStateController.refreshSessions() } return restoredSelection } private suspend fun runRollbackStep( Loading
app/src/test/java/foundation/e/apps/feature/auth/source/UpdateSourceSelectionCoordinatorTest.kt +36 −0 Original line number Diff line number Diff line Loading @@ -136,6 +136,38 @@ class UpdateSourceSelectionCoordinatorTest { } } @Test fun `invoke reports requested selection as restored when rollback save fails`() = runTest { val pendingUpdatesRepository = FakePendingUpdatesRepository() val sourceSelectionRepository = FakeSourceSelectionRepository( initialSourceSelection = SourceSelection.DEFAULT, saveFailureOnSelection = SourceSelection.DEFAULT, ) val periodicUpdatesScheduler = mockk<PeriodicUpdatesScheduler>() val sessionStateController = FakeSessionStateController() val coordinator = UpdateSourceSelectionCoordinator( pendingUpdatesRepository = pendingUpdatesRepository, periodicUpdatesScheduler = periodicUpdatesScheduler, sessionStateController = sessionStateController, sourceSelectionRepository = sourceSelectionRepository, ) val updatedSelection = SourceSelection.OPEN_SOURCE_AND_PWA coEvery { periodicUpdatesScheduler.syncPeriodicUpdates( existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE, ) } throws IOException("schedule failed") val exception = kotlin.test.assertFailsWith<SourceSelectionUpdateException> { coordinator(updatedSelection) } assertThat(exception.requestedSelection).isEqualTo(updatedSelection) assertThat(exception.restoredSelection).isEqualTo(updatedSelection) assertThat(sourceSelectionRepository.currentSourceSelection()).isEqualTo(updatedSelection) assertThat(exception.cause?.suppressed?.toList()).hasSize(1) } private class FakePendingUpdatesRepository : PendingUpdatesRepository { var clearPendingUpdatesCallCount = 0 Loading @@ -146,6 +178,7 @@ class UpdateSourceSelectionCoordinatorTest { private class FakeSourceSelectionRepository( initialSourceSelection: SourceSelection, private val saveFailureOnSelection: SourceSelection? = null, ) : SourceSelectionRepository { private val sourceSelectionState = MutableStateFlow(initialSourceSelection) Loading @@ -154,6 +187,9 @@ class UpdateSourceSelectionCoordinatorTest { override fun currentSourceSelection(): SourceSelection = sourceSelectionState.value override fun saveSourceSelection(sourceSelection: SourceSelection) { if (sourceSelection == saveFailureOnSelection) { throw IllegalStateException("save failed") } sourceSelectionState.value = sourceSelection } } Loading