Coverage Summary for Class: SyncCoordinatorWorkspaceSyncer (com.georgeci.moneysurfer.data.sync)

Class Method, % Branch, % Line, % Instruction, %
SyncCoordinatorWorkspaceSyncer 80% (4/5) 25% (5/20) 46.2% (12/26) 45% (103/229)
SyncCoordinatorWorkspaceSyncer$pushAll$1
SyncCoordinatorWorkspaceSyncer$syncAll$1
SyncCoordinatorWorkspaceSyncer$syncWorkspace$1
Total 80% (4/5) 25% (5/20) 46.2% (12/26) 45% (103/229)


 package com.georgeci.moneysurfer.data.sync
 
 import com.georgeci.moneysurfer.domain.auth.SessionMutator
 import com.georgeci.moneysurfer.domain.config.SyncSettings
 import com.georgeci.moneysurfer.domain.primitives.WorkspaceId
 import com.georgeci.moneysurfer.domain.repositories.WorkspaceSyncer
 import com.georgeci.moneysurfer.sync.api.SyncError
 import com.georgeci.moneysurfer.sync.api.SyncReason
 import com.georgeci.moneysurfer.sync.coordinator.SyncCoordinator
 import kotlinx.coroutines.flow.first
 import org.koin.core.annotation.Single
 
 /**
  * [WorkspaceSyncer] implementation that delegates to [SyncCoordinator].
  *
  * [pushAll] uses [SyncReason.LOCAL_CHANGE] → [SyncScope.UploadOnly]: drains the outbox
  * without a remote workspace-list fetch. Used post-create-workspace where the new workspace
  * and its member row need to land on Firestore before `addWorkspaceRef` is called.
  *
  * [syncAll] uses [SyncReason.MANUAL] → [SyncScope.AllUserData]: fetches
  * `users/{uid}.workspaceIds` via [UserWorkspacesProvider] and runs a cursor-based pull for
  * every workspace, including ones not yet in the local database.
  *
  * [syncWorkspace] sets the session's current workspace to the target workspace and
  * uses [SyncReason.SWIPE_REFRESH] → [SyncScope.ActiveWorkspace]: pulls only that workspace.
  * Used after accepting an invite so only the newly joined workspace is hydrated.
  */
 @Single(binds = [WorkspaceSyncer::class])
 class SyncCoordinatorWorkspaceSyncer(
     private val syncCoordinator: SyncCoordinator,
     private val sessionMutator: SessionMutator,
     private val syncSettings: SyncSettings,
 ) : WorkspaceSyncer {
 
     // When sync is off we want every use-case-driven trigger
     // (PostAuthBootstrap, CreateWorkspace, AcceptInvite, RefreshIncomingInvites)
     // to no-op instead of hitting Firestore. Gating at the syncer keeps the
     // use cases agnostic of the setting.
 
     override suspend fun pushAll(): Boolean {
         if (!syncSettings.isEnabled.first()) return false
         syncCoordinator.requestSync(SyncReason.LOCAL_CHANGE)
             .result.await()
             .fold(ifLeft = { throw it.toException() }, ifRight = { })
         return true
     }
 
     override suspend fun syncAll() {
         if (!syncSettings.isEnabled.first()) return
         syncCoordinator.requestSync(SyncReason.MANUAL)
             .result.await()
             .fold(ifLeft = { throw it.toException() }, ifRight = { })
     }
 
     override suspend fun syncWorkspace(workspaceId: WorkspaceId) {
         // Switch the active workspace pointer even when sync is off — callers
         // (e.g. AcceptInviteUseCase) rely on this to move the session to the
         // newly joined workspace. Only skip the network round-trip.
         sessionMutator.setCurrentWorkspace(workspaceId)
         if (!syncSettings.isEnabled.first()) return
         syncCoordinator.requestSync(SyncReason.SWIPE_REFRESH)
             .result.await()
             .fold(ifLeft = { throw it.toException() }, ifRight = { })
     }
 
     private fun SyncError.toException(): RuntimeException = when (this) {
         SyncError.Cancelled -> RuntimeException("Sync cancelled")
         SyncError.NetworkUnavailable -> RuntimeException("Network unavailable")
         SyncError.AuthRequired -> RuntimeException("Auth required")
         SyncError.PermissionDenied -> RuntimeException("PERMISSION_DENIED: workspace access denied")
         is SyncError.UnsupportedAppVersion -> RuntimeException(message)
         is SyncError.StorageError -> RuntimeException("Storage error", cause)
         is SyncError.Unknown -> RuntimeException(cause.message ?: "Sync failed", cause)
     }
 }