diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt index c55f7320da..2c61a294a6 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessor.kt @@ -19,6 +19,7 @@ import com.simprints.infra.eventsync.sync.common.didFailBecauseReloginRequired import com.simprints.infra.eventsync.sync.common.didFailBecauseTooManyRequests import com.simprints.infra.eventsync.sync.common.filterByTags import com.simprints.infra.eventsync.sync.common.getEstimatedOutageTime +import com.simprints.infra.eventsync.sync.common.hasAnyFailureReason import com.simprints.infra.eventsync.sync.common.sortByScheduledTime import com.simprints.infra.eventsync.sync.down.workers.extractDownSyncMaxCount import com.simprints.infra.eventsync.sync.down.workers.extractDownSyncProgress @@ -134,7 +135,7 @@ class EventSyncStateProcessor @Inject constructor( } private fun WorkInfo.toEventSyncWorkerState(): EventSyncWorkerState = fromWorkInfo( - state = state, + state = if (hasAnyFailureReason()) WorkInfo.State.FAILED else state, failedBecauseReloginRequired = didFailBecauseReloginRequired(), failedBecauseCloudIntegration = didFailBecauseCloudIntegration(), failedBecauseBackendMaintenance = didFailBecauseBackendMaintenance(), diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt index dd579123df..16623a5b9e 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/common/WorkInfo.ext.kt @@ -25,3 +25,10 @@ internal fun WorkInfo.didFailBecauseCommCarePermissionMissing(): Boolean = this.outputData.getBoolean(OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING, false) internal fun WorkInfo.getEstimatedOutageTime(): Long = this.outputData.getLong(OUTPUT_ESTIMATED_MAINTENANCE_TIME, 0L) + +internal fun WorkInfo.hasAnyFailureReason(): Boolean = + didFailBecauseReloginRequired() || + didFailBecauseCloudIntegration() || + didFailBecauseBackendMaintenance() || + didFailBecauseTooManyRequests() || + didFailBecauseCommCarePermissionMissing() diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt index 14a7f783c2..b74be5e1a7 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/BaseEventDownSyncTask.kt @@ -71,6 +71,7 @@ internal abstract class BaseEventDownSyncTask( val requestId = UUID.randomUUID().toString() var result: EventFetchResult? = null var errorType: String? = null + var errorToRethrow: Throwable? = null try { result = fetchEvents(operation, scope, requestId) @@ -98,11 +99,6 @@ internal abstract class BaseEventDownSyncTask( lastOperation = lastOperation.copy(state = COMPLETE, lastSyncTime = timeHelper.now().ms) emitProgress(lastOperation, count, result.totalCount) } catch (t: Throwable) { - if (shouldRethrowError(t)) { - throw t - } - - Simber.i("Down sync error", t, tag = SYNC) errorType = t.javaClass.simpleName lastOperation = processBatchedEvents(operation, batchOfEventsToProcess, lastOperation, project) @@ -110,6 +106,12 @@ internal abstract class BaseEventDownSyncTask( lastOperation = lastOperation.copy(state = FAILED, lastSyncTime = timeHelper.now().ms) emitProgress(lastOperation, count, count) + + if (shouldRethrowError(t)) { + errorToRethrow = t + } else { + Simber.i("Down sync error", t, tag = SYNC) + } } if (count > 0 || errorType != null) { @@ -136,6 +138,8 @@ internal abstract class BaseEventDownSyncTask( ), ) } + + errorToRethrow?.let { throw it } } private suspend fun FlowCollector.emitProgress( diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTask.kt index 419cd2871b..29276bec1b 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTask.kt @@ -6,9 +6,12 @@ import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.enrolment.records.repository.EnrolmentRecordRepository import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation import com.simprints.infra.eventsync.sync.common.EnrolmentRecordFactory +import com.simprints.infra.network.exceptions.BackendMaintenanceException +import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.consumeAsFlow @@ -59,6 +62,9 @@ internal class SimprintsEventDownSyncTask @Inject constructor( override fun shouldRethrowError(throwable: Throwable): Boolean { // Return true to re-throw specific exceptions that should not be handled by the base class - return throwable is RemoteDbNotSignedInException + return throwable is RemoteDbNotSignedInException || + throwable is BackendMaintenanceException || + throwable is SyncCloudIntegrationException || + throwable is TooManyRequestsException } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/BaseEventDownSyncDownloaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/BaseEventDownSyncDownloaderWorker.kt index 6815ac24aa..ea54461913 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/BaseEventDownSyncDownloaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/BaseEventDownSyncDownloaderWorker.kt @@ -5,6 +5,7 @@ import androidx.work.WorkInfo import androidx.work.WorkerParameters import androidx.work.workDataOf import com.simprints.core.workers.SimCoroutineWorker +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.enrolment.records.repository.local.migration.RealmToRoomMigrationFlagsStore import com.simprints.infra.events.EventRepository @@ -46,7 +47,11 @@ internal abstract class BaseEventDownSyncDownloaderWorker( abstract fun createDownSyncTask(): BaseEventDownSyncTask - abstract fun handleSyncException(t: Throwable): Result + abstract fun handleSyncException( + t: Throwable, + count: Int, + max: Int?, + ): Result override suspend fun doWork(): Result { // Check if the migration is in progress before starting the sync @@ -66,14 +71,14 @@ internal abstract class BaseEventDownSyncDownloaderWorker( protected open suspend fun performDownSync(): Result = withContext(dispatcher) { showProgressNotification() crashlyticsLog("Started") + val workerId = id.toString() + var count = syncCache.readProgress(workerId) + var max: Int? = syncCache.readMax(workerId) try { - val workerId = id.toString() - var count = syncCache.readProgress(workerId) - var max: Int? = syncCache.readMax(workerId) val project = configRepository.getProject() if (project == null) { - fail(IllegalStateException("User is not signed in")) + throw RemoteDbNotSignedInException() } else { createDownSyncTask().downSync(this, getDownSyncOperation(), getEventScope(), project).collect { count = it.progress @@ -93,7 +98,7 @@ internal abstract class BaseEventDownSyncDownloaderWorker( ) } } catch (t: Throwable) { - handleSyncException(t) + handleSyncException(t, count, max) } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorker.kt index b3afd0960a..9daab7735a 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorker.kt @@ -2,17 +2,20 @@ package com.simprints.infra.eventsync.sync.down.workers import android.content.Context import androidx.hilt.work.HiltWorker +import androidx.work.Data import androidx.work.WorkerParameters -import androidx.work.workDataOf import com.simprints.core.DispatcherBG +import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository import com.simprints.infra.enrolment.records.repository.local.migration.RealmToRoomMigrationFlagsStore import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING import com.simprints.infra.eventsync.sync.down.tasks.BaseEventDownSyncTask import com.simprints.infra.eventsync.sync.down.tasks.CommCareEventSyncTask +import com.simprints.infra.logging.Simber import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher @@ -40,9 +43,35 @@ internal class CommCareEventSyncDownloaderWorker @AssistedInject constructor( ) { override fun createDownSyncTask(): BaseEventDownSyncTask = commCareSyncTask - override fun handleSyncException(t: Throwable) = when (t) { - is IllegalArgumentException -> fail(t, t.message) - is SecurityException -> fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING to true)) - else -> retry(t) + override fun handleSyncException( + t: Throwable, + count: Int, + max: Int?, + ): Result { + val outputData = Data.Builder() + .putInt(OUTPUT_DOWN_SYNC, count) + .putInt(OUTPUT_DOWN_MAX_SYNC, max ?: 0) + + when (t) { + is SecurityException -> { + outputData.putBoolean(OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING, true) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + is RemoteDbNotSignedInException -> { + outputData.putBoolean(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED, true) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + is IllegalArgumentException -> { + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + else -> { + Simber.e("Down-sync completed with unexpected issue", t, tag = tag) + } + } + + return success(outputData.build(), "Completed with down-sync error: ${t.message}") } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorker.kt index 3bbc830f51..4ffc65fcca 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorker.kt @@ -2,8 +2,8 @@ package com.simprints.infra.eventsync.sync.down.workers import android.content.Context import androidx.hilt.work.HiltWorker +import androidx.work.Data import androidx.work.WorkerParameters -import androidx.work.workDataOf import com.simprints.core.DispatcherBG import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.config.store.ConfigRepository @@ -19,6 +19,7 @@ import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_R import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS import com.simprints.infra.eventsync.sync.down.tasks.BaseEventDownSyncTask import com.simprints.infra.eventsync.sync.down.tasks.SimprintsEventDownSyncTask +import com.simprints.infra.logging.Simber import com.simprints.infra.network.exceptions.BackendMaintenanceException import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import dagger.assisted.Assisted @@ -48,21 +49,47 @@ internal class SimprintsEventDownSyncDownloaderWorker @AssistedInject constructo ) { override fun createDownSyncTask(): BaseEventDownSyncTask = downSyncTask - override fun handleSyncException(t: Throwable) = when (t) { - is IllegalArgumentException -> fail(t, t.message) + override fun handleSyncException( + t: Throwable, + count: Int, + max: Int?, + ): Result { + val outputData = Data.Builder() + .putInt(OUTPUT_DOWN_SYNC, count) + .putInt(OUTPUT_DOWN_MAX_SYNC, max ?: 0) - is BackendMaintenanceException -> fail( - t, - t.message, - workDataOf( - OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, - OUTPUT_ESTIMATED_MAINTENANCE_TIME to t.estimatedOutage, - ), - ) + when (t) { + is BackendMaintenanceException -> { + outputData + .putBoolean(OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE, true) + .putLong(OUTPUT_ESTIMATED_MAINTENANCE_TIME, t.estimatedOutage ?: 0L) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } - is SyncCloudIntegrationException -> fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true)) - is TooManyRequestsException -> fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true)) - is RemoteDbNotSignedInException -> fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true)) - else -> retry(t) + is SyncCloudIntegrationException -> { + outputData.putBoolean(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION, true) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + is TooManyRequestsException -> { + outputData.putBoolean(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS, true) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + is RemoteDbNotSignedInException -> { + outputData.putBoolean(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED, true) + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + is IllegalArgumentException -> { + Simber.i("Down-sync completed with recoverable issue", t, tag = tag) + } + + else -> { + Simber.e("Down-sync completed with unexpected issue", t, tag = tag) + } + } + + return success(outputData.build(), "Completed with down-sync error: ${t.message}") } } diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt index 8a5dce714a..267eb20a89 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorker.kt @@ -9,10 +9,13 @@ import com.simprints.core.workers.SimCoroutineWorker import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScopeEndCause import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.SyncWorkersInfoProvider +import com.simprints.infra.eventsync.sync.common.hasAnyFailureReason import com.simprints.infra.logging.Simber import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext /** @@ -25,6 +28,7 @@ internal class EventEndSyncReporterWorker @AssistedInject constructor( @Assisted params: WorkerParameters, private val syncCache: EventSyncCache, private val eventRepository: EventRepository, + private val syncWorkersInfoProvider: SyncWorkersInfoProvider, private val timeHelper: TimeHelper, @param:DispatcherBG private val dispatcher: CoroutineDispatcher, ) : SimCoroutineWorker(appContext, params) { @@ -46,7 +50,15 @@ internal class EventEndSyncReporterWorker @AssistedInject constructor( } if (!syncId.isNullOrEmpty()) { - syncCache.storeLastSuccessfulSyncTime(timeHelper.now()) + val hasWorkerFailures = syncWorkersInfoProvider + .getSyncWorkerInfos(syncId) + .firstOrNull() + .orEmpty() + .any { it.hasAnyFailureReason() } + + if (!hasWorkerFailures) { + syncCache.storeLastSuccessfulSyncTime(timeHelper.now()) + } success() } else { throw IllegalArgumentException("SyncId missed") diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt index acb4f83d8f..bb7b4359e4 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTask.kt @@ -25,6 +25,7 @@ import com.simprints.infra.events.event.domain.models.scope.EventScope import com.simprints.infra.events.event.domain.models.scope.EventScopeType import com.simprints.infra.eventsync.event.remote.ApiUploadEventsBody import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.event.remote.models.session.ApiEventScope import com.simprints.infra.eventsync.event.usecases.MapDomainEventScopeToApiUseCase import com.simprints.infra.eventsync.exceptions.TryToUploadEventsForNotSignedProject @@ -37,7 +38,9 @@ import com.simprints.infra.eventsync.status.up.domain.EventUpSyncResult import com.simprints.infra.eventsync.sync.up.EventUpSyncProgress import com.simprints.infra.logging.LoggingConstants.CrashReportTag.SYNC import com.simprints.infra.logging.Simber +import com.simprints.infra.network.exceptions.BackendMaintenanceException import com.simprints.infra.network.exceptions.NetworkConnectionException +import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import com.simprints.infra.serialization.SimJson import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow @@ -173,10 +176,6 @@ internal class EventUpSyncTask @Inject constructor( emitProgress(lastOperation, 0) } catch (t: Throwable) { - if (t is RemoteDbNotSignedInException) { - throw t - } - Simber.e("Failed to upload event scopes", t, tag = SYNC) lastOperation = lastOperation.copy( lastState = FAILED, @@ -184,6 +183,9 @@ internal class EventUpSyncTask @Inject constructor( ) emitProgress(lastOperation, 0) + if (isTerminalException(t)) { + throw t + } } } @@ -195,6 +197,11 @@ internal class EventUpSyncTask @Inject constructor( this.emit(EventUpSyncProgress(lastOperation, count)) } + private fun isTerminalException(t: Throwable): Boolean = t is RemoteDbNotSignedInException || + t is BackendMaintenanceException || + t is SyncCloudIntegrationException || + t is TooManyRequestsException + private fun uploadEventScopeType( eventScope: EventScope, project: Project, diff --git a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt index 9bf51a51ea..67a64f26d5 100644 --- a/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt +++ b/infra/event-sync/src/main/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorker.kt @@ -2,6 +2,7 @@ package com.simprints.infra.eventsync.sync.up.workers import android.content.Context import androidx.hilt.work.HiltWorker +import androidx.work.Data import androidx.work.WorkInfo import androidx.work.WorkerParameters import androidx.work.workDataOf @@ -10,6 +11,7 @@ import com.simprints.core.workers.SimCoroutineWorker import com.simprints.infra.authstore.AuthStore import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.exceptions.MalformedSyncOperationException import com.simprints.infra.eventsync.status.up.domain.EventUpSyncScope import com.simprints.infra.eventsync.sync.common.EventSyncCache @@ -17,6 +19,7 @@ import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TI import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS import com.simprints.infra.eventsync.sync.common.WorkerProgressCountReporter import com.simprints.infra.eventsync.sync.up.tasks.EventUpSyncTask import com.simprints.infra.eventsync.sync.up.workers.EventUpSyncUploaderWorker.Companion.OUTPUT_UP_MAX_SYNC @@ -70,12 +73,12 @@ internal class EventUpSyncUploaderWorker @AssistedInject constructor( override suspend fun doWork(): Result = withContext(dispatcher) { showProgressNotification() crashlyticsLog("Started") + val workerId = this@EventUpSyncUploaderWorker.id.toString() + var count = eventSyncCache.readProgress(workerId) + var max = count + try { - val workerId = this@EventUpSyncUploaderWorker.id.toString() - var count = eventSyncCache.readProgress(workerId) - val max = eventRepository - .observeEventCountInClosedScopes() - .firstOrNull() ?: 0 + max = eventRepository.observeEventCountInClosedScopes().firstOrNull() ?: 0 upSyncTask.upSync(upSyncScope.operation, getEventScope()).collect { count += it.progress @@ -86,44 +89,39 @@ internal class EventUpSyncUploaderWorker @AssistedInject constructor( } success( - workDataOf( - OUTPUT_UP_SYNC to count, - OUTPUT_UP_MAX_SYNC to max, - ), + createOutputData(count, max), "Total uploaded: $count / $max", ) } catch (t: Throwable) { - retryOrFailIfCloudIntegrationOrBackendMaintenanceError(t) - } - } - - private fun retryOrFailIfCloudIntegrationOrBackendMaintenanceError(t: Throwable) = when (t) { - is IllegalArgumentException -> { - fail(t, t.message) - } - - is BackendMaintenanceException -> { - fail( - t, - t.message, - workDataOf( - OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, - OUTPUT_ESTIMATED_MAINTENANCE_TIME to t.estimatedOutage, - ), + Simber.i("Up-sync completed with issue", t, tag = tag) + success( + createOutputData(count, max, t), + "Completed with up-sync error: ${t.message}", ) } + } - is SyncCloudIntegrationException -> { - fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true)) - } - - is RemoteDbNotSignedInException -> { - fail(t, t.message, workDataOf(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true)) + private fun createOutputData( + count: Int, + max: Int, + t: Throwable? = null, + ): Data { + val outputDataBuilder = Data + .Builder() + .putInt(OUTPUT_UP_SYNC, count) + .putInt(OUTPUT_UP_MAX_SYNC, max) + + when (t) { + is SyncCloudIntegrationException -> outputDataBuilder.putBoolean(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION, true) + is RemoteDbNotSignedInException -> outputDataBuilder.putBoolean(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED, true) + is TooManyRequestsException -> outputDataBuilder.putBoolean(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS, true) + is BackendMaintenanceException -> + outputDataBuilder + .putBoolean(OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE, true) + .putLong(OUTPUT_ESTIMATED_MAINTENANCE_TIME, t.estimatedOutage ?: 0L) } - else -> { - retry(t) - } + return outputDataBuilder.build() } override suspend fun reportCount( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt index 08cec7b665..ae21388082 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/EventSyncStateProcessorTest.kt @@ -6,7 +6,7 @@ import androidx.work.WorkInfo import androidx.work.WorkInfo.State.FAILED import androidx.work.WorkInfo.State.SUCCEEDED import androidx.work.workDataOf -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.core.tools.time.Timestamp import com.simprints.infra.eventsync.status.models.EventSyncWorkerState import com.simprints.infra.eventsync.status.models.EventSyncWorkerType.Companion.tagForType @@ -203,6 +203,93 @@ internal class EventSyncStateProcessorTest { .assertEqualToFailedState(expectedState) } + @Test + fun getLastSyncState_shouldMapUploaderFailureFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithUploaderReloginError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed(failedBecauseReloginRequired = true) + syncStates.upSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + + @Test + fun getLastSyncState_shouldMapTooManyRequestsFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithUploaderTooManyRequestsError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed(failedBecauseTooManyRequest = true) + syncStates.upSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + + @Test + fun getLastSyncState_shouldMapCommCarePermissionFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithDownloaderCommCarePermissionError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed(failedBecauseCommCarePermissionMissing = true) + syncStates.downSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + + @Test + fun getLastSyncState_shouldMapDownloaderBackendMaintenanceFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithDownloaderBackendMaintenanceError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed( + failedBecauseBackendMaintenance = true, + estimatedOutage = 6, + ) + syncStates.downSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + + @Test + fun getLastSyncState_shouldMapDownloaderTooManyRequestsFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithDownloaderTooManyRequestsError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed(failedBecauseTooManyRequest = true) + syncStates.downSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + + @Test + fun getLastSyncState_shouldMapDownloaderCloudIntegrationFlagsEvenWhenWorkerSucceeded() = runTest { + startSyncReporterWorker.emit(successfulMasterWorkers) + syncWorkersFlow.emit(createWorkInfosHistoryForSucceededSyncWithDownloaderCloudIntegrationError()) + + val syncStates = eventSyncStateProcessor.getLastSyncState().first() + + val expectedState = EventSyncWorkerState.Failed(failedBecauseCloudIntegration = true) + syncStates.downSyncWorkersInfo + .first() + .state + .assertEqualToFailedState(expectedState) + } + private fun createStartSyncReporterWorker( state: WorkInfo.State = SUCCEEDED, uniqueMasterSyncId: String, diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt index 7e5bc803cb..a662420001 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/SubjectsSyncStateProcessorTestHelper.kt @@ -27,6 +27,8 @@ import com.simprints.infra.eventsync.sync.EventSyncStateProcessorTest.Companion. import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TIME import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS import com.simprints.infra.eventsync.sync.common.TAG_DOWN_MASTER_SYNC_ID import com.simprints.infra.eventsync.sync.common.TAG_MASTER_SYNC_ID @@ -49,9 +51,11 @@ fun EventSyncWorkerState.assertEqualToFailedState(e: Failed) { assertThat(this).isInstanceOf(Failed::class.java) val failed = this as Failed assertThat(failed.estimatedOutage).isEqualTo(e.estimatedOutage) + assertThat(failed.failedBecauseReloginRequired).isEqualTo(e.failedBecauseReloginRequired) assertThat(failed.failedBecauseCloudIntegration).isEqualTo(e.failedBecauseCloudIntegration) assertThat(failed.failedBecauseBackendMaintenance).isEqualTo(e.failedBecauseBackendMaintenance) assertThat(failed.failedBecauseTooManyRequest).isEqualTo(e.failedBecauseTooManyRequest) + assertThat(failed.failedBecauseCommCarePermissionMissing).isEqualTo(e.failedBecauseCommCarePermissionMissing) } fun EventSyncState.assertConnectingSyncState() { @@ -145,6 +149,63 @@ fun createWorkInfosHistoryForFailingSyncDueCloudIntegrationError(): List = listOf( + createDownSyncDownloaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), + createUpSyncUploaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + output = workDataOf(OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true), + ), +) + +fun createWorkInfosHistoryForSucceededSyncWithUploaderTooManyRequestsError(): List = listOf( + createDownSyncDownloaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), + createUpSyncUploaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + output = workDataOf(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true), + ), +) + +fun createWorkInfosHistoryForSucceededSyncWithDownloaderCommCarePermissionError(): List = listOf( + createDownSyncDownloaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + workDataOf(OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING to true), + ), + createUpSyncUploaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), +) + +fun createWorkInfosHistoryForSucceededSyncWithDownloaderBackendMaintenanceError(): List = listOf( + createDownSyncDownloaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + workDataOf( + OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, + OUTPUT_ESTIMATED_MAINTENANCE_TIME to 6L, + ), + ), + createUpSyncUploaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), +) + +fun createWorkInfosHistoryForSucceededSyncWithDownloaderTooManyRequestsError(): List = listOf( + createDownSyncDownloaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + workDataOf(OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true), + ), + createUpSyncUploaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), +) + +fun createWorkInfosHistoryForSucceededSyncWithDownloaderCloudIntegrationError(): List = listOf( + createDownSyncDownloaderWorker( + SUCCEEDED, + UNIQUE_SYNC_ID, + workDataOf(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true), + ), + createUpSyncUploaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), +) + fun createWorkInfosHistoryForConnectingSync(): List = listOf( createDownSyncDownloaderWorker(ENQUEUED, UNIQUE_SYNC_ID), createUpSyncUploaderWorker(SUCCEEDED, UNIQUE_SYNC_ID), @@ -176,13 +237,17 @@ private fun createDownSyncDownloaderWorker( private fun createUpSyncUploaderWorker( state: WorkInfo.State, uniqueMasterSyncId: String?, + output: Data = workDataOf(), uniqueSyncId: String? = UNIQUE_UP_SYNC_ID, id: UUID = UUID.randomUUID(), ) = createWorkInfo( state, - workDataOf( - OUTPUT_UP_SYNC to UPLOADED, - OUTPUT_UP_MAX_SYNC to TO_UPLOAD, + concatData( + workDataOf( + OUTPUT_UP_SYNC to UPLOADED, + OUTPUT_UP_MAX_SYNC to TO_UPLOAD, + ), + output, ), createCommonUpSyncTags(uniqueMasterSyncId, uniqueSyncId) + setOf(tagForType(UPLOADER)), workDataOf( diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EnrolmentRecordFactoryTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EnrolmentRecordFactoryTest.kt index 04eaf73869..ddf3e2dd8e 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EnrolmentRecordFactoryTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/EnrolmentRecordFactoryTest.kt @@ -25,11 +25,9 @@ import com.simprints.infra.events.sampledata.SampleDefaults.GUID1 import com.simprints.infra.eventsync.sync.common.EnrolmentRecordFactory import io.mockk.* import io.mockk.impl.annotations.MockK -import org.junit.After import org.junit.Before import org.junit.Test import java.util.Date -import java.util.UUID class EnrolmentRecordFactoryTest { @MockK @@ -41,7 +39,6 @@ class EnrolmentRecordFactoryTest { @Before fun setup() { MockKAnnotations.init(this, relaxed = true) - mockkStatic(UUID::class) every { encodingUtils.base64ToBytes(any()) } returns BASE_64_BYTES factory = EnrolmentRecordFactory( @@ -50,11 +47,6 @@ class EnrolmentRecordFactoryTest { ) } - @After - fun tearDown() { - unmockkStatic(UUID::class) - } - @Test fun `when buildFromCreationPayload is called, correct samples are built`() { val payload = EnrolmentRecordCreationEvent.EnrolmentRecordCreationPayload( @@ -279,7 +271,6 @@ class EnrolmentRecordFactoryTest { @Test fun `when buildFromCaptureResults is called, correct subject is built`() { val randomUUID = "5a95b24d-23c5-4d24-9277-0d3d2a287508" // "chosen by fair dice roll. guaranteed to be random" (c) xkcd - every { UUID.randomUUID().toString() } returns randomUUID val expected = EnrolmentRecord( subjectId = randomUUID, diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt index 1414e7e2d9..4a125853fe 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/tasks/SimprintsEventDownSyncTaskTest.kt @@ -35,6 +35,7 @@ import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID import com.simprints.infra.events.sampledata.SampleDefaults.DEFAULT_USER_ID_2 import com.simprints.infra.eventsync.SampleSyncScopes import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation.DownSyncState.COMPLETE import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation.DownSyncState.FAILED @@ -42,6 +43,8 @@ import com.simprints.infra.eventsync.status.down.domain.EventDownSyncOperation.D import com.simprints.infra.eventsync.status.down.domain.EventDownSyncResult import com.simprints.infra.eventsync.sync.common.EnrolmentRecordFactory import com.simprints.infra.eventsync.sync.down.tasks.BaseEventDownSyncTask.Companion.EVENTS_BATCH_SIZE +import com.simprints.infra.network.exceptions.BackendMaintenanceException +import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import com.simprints.testtools.common.coroutines.TestCoroutineRule import com.simprints.testtools.unit.EncodingUtilsImplForTests import io.mockk.* @@ -280,6 +283,50 @@ class SimprintsEventDownSyncTaskTest { eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() } + @Test(expected = BackendMaintenanceException::class) + fun downSync_shouldThrowUpIfBackendMaintenanceExceptionOccurs() = runTest { + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws BackendMaintenanceException(estimatedOutage = null) + + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + } + + @Test(expected = SyncCloudIntegrationException::class) + fun downSync_shouldThrowUpIfSyncCloudIntegrationExceptionOccurs() = runTest { + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws SyncCloudIntegrationException("Cloud integration", Throwable()) + + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + } + + @Test(expected = TooManyRequestsException::class) + fun downSync_shouldThrowUpIfTooManyRequestsExceptionOccurs() = runTest { + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws TooManyRequestsException() + + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + } + + @Test + fun downSync_shouldLogRequestEventBeforeRethrowingCloudIntegrationException() = runTest { + val expectedException = SyncCloudIntegrationException("Cloud integration", Throwable()) + coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws expectedException + + try { + eventDownSyncTask.downSync(this, projectOp, eventScope, project).toList() + throw AssertionError("Expected SyncCloudIntegrationException") + } catch (ex: SyncCloudIntegrationException) { + assertThat(ex).isEqualTo(expectedException) + } + + coVerify(exactly = 1) { + eventRepository.addOrUpdateEvent( + eventScope, + match { + it is EventDownSyncRequestEvent && + it.payload.errorType == expectedException.javaClass.simpleName + }, + ) + } + } + @Test fun downSync_shouldAddEventWithErrorIfDownloadFails() = runTest { coEvery { eventRemoteDataSource.getEvents(any(), any(), any()) } throws Throwable("IO Exception") diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorkerTest.kt index 49265eb874..0e0aa8cc8c 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/CommCareEventSyncDownloaderWorkerTest.kt @@ -16,9 +16,11 @@ import com.simprints.infra.eventsync.SampleSyncScopes.projectDownSyncScope import com.simprints.infra.eventsync.status.down.EventDownSyncScopeRepository import com.simprints.infra.eventsync.sync.common.EventSyncCache import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED import com.simprints.infra.eventsync.sync.down.tasks.CommCareEventSyncTask import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.INPUT_DOWN_SYNC_OPS import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.INPUT_EVENT_DOWN_SYNC_SCOPE_ID +import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_MAX_SYNC import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_SYNC import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.PROGRESS_DOWN_SYNC import com.simprints.infra.serialization.SimJson @@ -119,16 +121,40 @@ internal class CommCareEventSyncDownloaderWorkerTest { } @Test - fun `worker with no event scope should fail`() = runTest { + fun `worker with no event scope should succeed without failure flags`() = runTest { coEvery { eventRepository.getEventScope(any()) } returns null val result = eventDownSyncDownloaderWorker.doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.failure()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, + ), + ), + ) } @Test - fun `worker should fail if task throws IllegalArgumentException`() = runTest { + fun `worker with no project should succeed with relogin flag`() = runTest { + coEvery { configRepository.getProject() } returns null + + val result = eventDownSyncDownloaderWorker.doWork() + + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, + OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true, + ), + ), + ) + } + + @Test + fun `worker should succeed without failure flags if task throws IllegalArgumentException`() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { commCareSyncTask.downSync(any(), any(), any(), any()) @@ -136,11 +162,18 @@ internal class CommCareEventSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.failure()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, + ), + ), + ) } @Test - fun `worker should fail if task throws SecurityException`() = runTest { + fun `worker should set permission-missing failure flag if task throws SecurityException`() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { commCareSyncTask.downSync(any(), any(), any(), any()) @@ -149,8 +182,10 @@ internal class CommCareEventSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_COMMCARE_PERMISSION_MISSING to true, ), ), diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorkerTest.kt index 13b4e512c5..730e5cdde4 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/down/workers/SimprintsEventDownSyncDownloaderWorkerTest.kt @@ -25,6 +25,7 @@ import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_ import com.simprints.infra.eventsync.sync.down.tasks.SimprintsEventDownSyncTask import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.INPUT_DOWN_SYNC_OPS import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.INPUT_EVENT_DOWN_SYNC_SCOPE_ID +import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_MAX_SYNC import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.OUTPUT_DOWN_SYNC import com.simprints.infra.eventsync.sync.down.workers.BaseEventDownSyncDownloaderWorker.Companion.PROGRESS_DOWN_SYNC import com.simprints.infra.network.exceptions.BackendMaintenanceException @@ -130,16 +131,23 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { } @Test - fun worker_noEventScope_shouldFail() = runTest { + fun worker_noEventScope_shouldSucceedWithoutFailureFlags() = runTest { coEvery { eventRepository.getEventScope(any()) } returns null val result = eventDownSyncDownloaderWorker.doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.failure()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, + ), + ), + ) } @Test - fun worker_failForCloudIntegration_shouldFail() = runTest { + fun worker_failForCloudIntegration_shouldSetFailureFlag() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -148,8 +156,10 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true, ), ), @@ -157,7 +167,7 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { } @Test - fun worker_failForBackendMaintenanceError_shouldFail() = runTest { + fun worker_failForBackendMaintenanceError_shouldSetFailureFlag() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -166,17 +176,19 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, - OUTPUT_ESTIMATED_MAINTENANCE_TIME to null, + OUTPUT_ESTIMATED_MAINTENANCE_TIME to 0L, ), ), ) } @Test - fun worker_failForTimedBackendMaintenanceError_shouldFail() = runTest { + fun worker_failForTimedBackendMaintenanceError_shouldSetFailureFlag() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -185,8 +197,10 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, OUTPUT_ESTIMATED_MAINTENANCE_TIME to 600L, ), @@ -195,7 +209,7 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { } @Test - fun worker_failForTooManyRequestsError_shouldFail() = runTest { + fun worker_failForTooManyRequestsError_shouldSetFailureFlag() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -204,8 +218,10 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true, ), ), @@ -213,7 +229,7 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { } @Test - fun worker_failForRemoteDbNotSignedInException_shouldFail() = runTest { + fun worker_failForRemoteDbNotSignedInException_shouldSetFailureFlag() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -222,8 +238,10 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true, ), ), @@ -231,7 +249,7 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { } @Test - fun worker_failForNetworkIssue_shouldRetry() = runTest { + fun worker_failForNetworkIssue_shouldSucceedWithoutFailureFlags() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { downSyncTask.downSync(any(), any(), any(), any()) @@ -239,7 +257,14 @@ internal class SimprintsEventDownSyncDownloaderWorkerTest { val result = eventDownSyncDownloaderWorker.doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.retry()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + OUTPUT_DOWN_SYNC to 0, + OUTPUT_DOWN_MAX_SYNC to 0, + ), + ), + ) } @Test diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorkerTest.kt index 518be080d7..df6ec44aae 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/master/EventEndSyncReporterWorkerTest.kt @@ -8,11 +8,15 @@ import com.simprints.core.tools.time.TimeHelper import com.simprints.core.tools.time.Timestamp import com.simprints.infra.events.EventRepository import com.simprints.infra.eventsync.sync.common.EventSyncCache +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION +import com.simprints.infra.eventsync.sync.common.SyncWorkersInfoProvider import com.simprints.infra.eventsync.sync.master.EventEndSyncReporterWorker.Companion.EVENT_DOWN_SYNC_SCOPE_TO_CLOSE +import com.simprints.infra.eventsync.sync.master.EventEndSyncReporterWorker.Companion.EVENT_UP_SYNC_SCOPE_TO_CLOSE import com.simprints.infra.eventsync.sync.master.EventEndSyncReporterWorker.Companion.SYNC_ID_TO_MARK_AS_COMPLETED import com.simprints.testtools.common.coroutines.TestCoroutineRule import io.mockk.* import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Rule @@ -31,10 +35,14 @@ internal class EventEndSyncReporterWorkerTest { @MockK lateinit var eventRepository: EventRepository + @MockK + lateinit var syncWorkersInfoProvider: SyncWorkersInfoProvider + @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) every { timeHelper.now() } returns Timestamp(1) + every { syncWorkersInfoProvider.getSyncWorkerInfos(any()) } returns flowOf(emptyList()) } @Test @@ -82,6 +90,23 @@ internal class EventEndSyncReporterWorkerTest { coVerify(exactly = 1) { eventRepository.closeEventScope("scopeId", any()) } } + @Test + fun `doWork should not save last success time when workers contain failure flags`() = runTest { + every { syncWorkersInfoProvider.getSyncWorkerInfos(any()) } returns flowOf( + listOf( + mockk(relaxed = true) { + every { outputData } returns workDataOf(OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true) + }, + ), + ) + + val endSyncReportWorker = createWorker("sync id", null, null) + val result = endSyncReportWorker.doWork() + + assertThat(result).isEqualTo(ListenableWorker.Result.success()) + coVerify(exactly = 0) { syncCache.storeLastSuccessfulSyncTime(any()) } + } + private fun createWorker( syncId: String?, downScopeId: String?, @@ -96,11 +121,12 @@ internal class EventEndSyncReporterWorkerTest { every { inputData } returns workDataOf( SYNC_ID_TO_MARK_AS_COMPLETED to syncId, EVENT_DOWN_SYNC_SCOPE_TO_CLOSE to downScopeId, - EVENT_DOWN_SYNC_SCOPE_TO_CLOSE to upScopeId, + EVENT_UP_SYNC_SCOPE_TO_CLOSE to upScopeId, ) }, syncCache, eventRepository, + syncWorkersInfoProvider, timeHelper, testCoroutineRule.testCoroutineDispatcher, ) diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt index 943bfcccff..cb1953b3cc 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/tasks/EventUpSyncTaskTest.kt @@ -34,12 +34,15 @@ import com.simprints.infra.events.sampledata.createPersonCreationEvent import com.simprints.infra.events.sampledata.createSessionScope import com.simprints.infra.eventsync.SampleSyncScopes import com.simprints.infra.eventsync.event.remote.EventRemoteDataSource +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.event.usecases.MapDomainEventScopeToApiUseCase import com.simprints.infra.eventsync.exceptions.TryToUploadEventsForNotSignedProject import com.simprints.infra.eventsync.status.up.EventUpSyncScopeRepository import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation.UpSyncState +import com.simprints.infra.network.exceptions.BackendMaintenanceException import com.simprints.infra.network.exceptions.NetworkConnectionException +import com.simprints.infra.network.exceptions.SyncCloudIntegrationException import io.mockk.* import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.toList @@ -689,6 +692,27 @@ internal class EventUpSyncTaskTest { eventUpSyncTask.upSync(operation, eventScope).toList() } + @Test(expected = BackendMaintenanceException::class) + fun `upSync should throw up if BackendMaintenanceException occurs`() = runTest { + coEvery { eventRepo.getClosedEventScopesCount(any()) } throws BackendMaintenanceException(estimatedOutage = null) + + eventUpSyncTask.upSync(operation, eventScope).toList() + } + + @Test(expected = SyncCloudIntegrationException::class) + fun `upSync should throw up if SyncCloudIntegrationException occurs`() = runTest { + coEvery { eventRepo.getClosedEventScopesCount(any()) } throws SyncCloudIntegrationException("", Throwable()) + + eventUpSyncTask.upSync(operation, eventScope).toList() + } + + @Test(expected = TooManyRequestsException::class) + fun `upSync should throw up if TooManyRequestsException occurs`() = runTest { + coEvery { eventRepo.getClosedEventScopesCount(any()) } throws TooManyRequestsException() + + eventUpSyncTask.upSync(operation, eventScope).toList() + } + private fun setUpSyncKind(kind: UpSynchronizationConfiguration.UpSynchronizationKind) { every { synchronizationConfiguration.up.simprints.kind } returns kind } diff --git a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt index 16579295a2..897d4ca671 100644 --- a/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt +++ b/infra/event-sync/src/test/java/com/simprints/infra/eventsync/sync/up/workers/EventUpSyncUploaderWorkerTest.kt @@ -14,6 +14,7 @@ import com.simprints.infra.authstore.AuthStore import com.simprints.infra.authstore.exceptions.RemoteDbNotSignedInException import com.simprints.infra.events.EventRepository import com.simprints.infra.events.event.domain.models.scope.EventScope +import com.simprints.infra.eventsync.event.remote.exceptions.TooManyRequestsException import com.simprints.infra.eventsync.status.up.domain.EventUpSyncOperation import com.simprints.infra.eventsync.status.up.domain.EventUpSyncScope import com.simprints.infra.eventsync.sync.common.EventSyncCache @@ -21,6 +22,7 @@ import com.simprints.infra.eventsync.sync.common.OUTPUT_ESTIMATED_MAINTENANCE_TI import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED +import com.simprints.infra.eventsync.sync.common.OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS import com.simprints.infra.eventsync.sync.up.EventUpSyncProgress import com.simprints.infra.eventsync.sync.up.tasks.EventUpSyncTask import com.simprints.infra.eventsync.sync.up.workers.EventUpSyncUploaderWorker.Companion.INPUT_EVENT_UP_SYNC_SCOPE_ID @@ -101,16 +103,23 @@ internal class EventUpSyncUploaderWorkerTest { } @Test - fun worker_shouldFailCorrectlyIfNoEventScope() = runTest { + fun worker_shouldSucceedWithoutFlagsIfNoEventScope() = runTest { coEvery { eventRepository.getEventScope(any()) } returns null val result = init(projectScope).doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.failure()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, + ), + ), + ) } @Test - fun worker_shouldSetFailCorrectlyIfBackendError() = runTest { + fun worker_shouldSetFailureFlagCorrectlyIfBackendError() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { upSyncTask.upSync(any(), eventScope) @@ -119,17 +128,19 @@ internal class EventUpSyncUploaderWorkerTest { val result = init(projectScope).doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, - OUTPUT_ESTIMATED_MAINTENANCE_TIME to null, + OUTPUT_ESTIMATED_MAINTENANCE_TIME to 0L, ), ), ) } @Test - fun worker_shouldSetFailCorrectlyIfTimedBackendError() = runTest { + fun worker_shouldSetFailureFlagCorrectlyIfTimedBackendError() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { upSyncTask.upSync(any(), eventScope) @@ -138,8 +149,10 @@ internal class EventUpSyncUploaderWorkerTest { val result = init(projectScope).doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, OUTPUT_FAILED_BECAUSE_BACKEND_MAINTENANCE to true, OUTPUT_ESTIMATED_MAINTENANCE_TIME to 600L, ), @@ -148,7 +161,7 @@ internal class EventUpSyncUploaderWorkerTest { } @Test - fun worker_shouldSetFailCorrectlyIfCloudIntegrationError() = runTest { + fun worker_shouldSetFailureFlagCorrectlyIfCloudIntegrationError() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { upSyncTask.upSync(any(), eventScope) @@ -157,8 +170,10 @@ internal class EventUpSyncUploaderWorkerTest { val result = init(projectScope).doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, OUTPUT_FAILED_BECAUSE_CLOUD_INTEGRATION to true, ), ), @@ -166,7 +181,7 @@ internal class EventUpSyncUploaderWorkerTest { } @Test - fun worker_shouldSetFailCorrectlyIfRemoteDbNotSignedInException() = runTest { + fun worker_shouldSetFailureFlagCorrectlyIfRemoteDbNotSignedInException() = runTest { val eventUpSyncUploaderWorker = init(projectScope) coEvery { @@ -176,8 +191,10 @@ internal class EventUpSyncUploaderWorkerTest { val result = eventUpSyncUploaderWorker.doWork() assertThat(result).isEqualTo( - ListenableWorker.Result.failure( + ListenableWorker.Result.success( workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, OUTPUT_FAILED_BECAUSE_RELOGIN_REQUIRED to true, ), ), @@ -185,7 +202,27 @@ internal class EventUpSyncUploaderWorkerTest { } @Test - fun worker_shouldRetryIfNotBackendMaintenanceOrSyncIssue() = runTest { + fun worker_shouldSetFailureFlagCorrectlyIfTooManyRequests() = runTest { + coEvery { eventRepository.getEventScope(any()) } returns eventScope + coEvery { + upSyncTask.upSync(any(), eventScope) + } throws TooManyRequestsException() + + val result = init(projectScope).doWork() + + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, + OUTPUT_FAILED_BECAUSE_TOO_MANY_REQUESTS to true, + ), + ), + ) + } + + @Test + fun worker_shouldSucceedWithoutFlagsIfUnexpectedIssue() = runTest { coEvery { eventRepository.getEventScope(any()) } returns eventScope coEvery { upSyncTask.upSync(any(), eventScope) @@ -193,7 +230,14 @@ internal class EventUpSyncUploaderWorkerTest { val result = init(projectScope).doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.retry()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, + ), + ), + ) } @Test @@ -238,12 +282,19 @@ internal class EventUpSyncUploaderWorkerTest { } @Test - fun `should retry when input is null`() = runTest { + fun `should succeed when input is null`() = runTest { val eventUpSyncUploaderWorker = init(null) val result = eventUpSyncUploaderWorker.doWork() - assertThat(result).isEqualTo(ListenableWorker.Result.retry()) + assertThat(result).isEqualTo( + ListenableWorker.Result.success( + workDataOf( + EventUpSyncUploaderWorker.OUTPUT_UP_SYNC to 0, + EventUpSyncUploaderWorker.OUTPUT_UP_MAX_SYNC to 12, + ), + ), + ) } // We are using the TestListenableWorkerBuilder and not the constructor directly to have a test worker