Coverage Summary for Class: SyncErrorClassifierKt (com.georgeci.moneysurfer.data.sync)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SyncErrorClassifierKt |
100%
(1/1)
|
66.7%
(2/3)
|
26.7%
(8/30)
|
43.8%
(7/16)
|
32.9%
(52/158)
|
package com.georgeci.moneysurfer.data.sync
import com.georgeci.moneysurfer.sync.api.SyncError
import dev.gitlive.firebase.firestore.FirebaseFirestoreException
import kotlinx.serialization.SerializationException
import kotlin.coroutines.cancellation.CancellationException
/**
* Maps SDK / runtime exceptions into a typed [SyncError].
*
* Lives in `:data` because only here do we see vendor-specific classes
* (Firestore, Room). The coordinator and use case interfaces stay
* vendor-neutral. See sync.md §4.2 «Конвенция error handling».
*/
internal fun Throwable.toSyncError(): SyncError = when (this) {
is CancellationException -> SyncError.Cancelled
is FirebaseFirestoreException -> classifyFirestore(this)
is SerializationException -> SyncError.Unknown(this)
else -> SyncError.Unknown(this)
}
/**
* A denial, as opposed to "not here *yet*". The distinction decides whether a caller may skip what
* it was reading: a denied read will never succeed for this user, while a network drop or an
* expired token would turn skipping into a sync that reports success and downloaded nothing.
*
* The gitlive wrapper does not surface a typed `FirebaseFirestoreException` uniformly across
* platforms, so this falls back to the message the same way [toSyncError] does.
*/
internal fun Throwable.isPermissionDenied(): Boolean =
toSyncError() == SyncError.PermissionDenied ||
PERMISSION_DENIED_MARKER in message?.uppercase().orEmpty()
private const val PERMISSION_DENIED_MARKER: String = "PERMISSION_DENIED"
private fun classifyFirestore(error: FirebaseFirestoreException): SyncError {
// gitlive does not surface a typed Code enum across all platforms uniformly,
// so fall back to message inspection until we wire a dedicated mapper per
// platform. Expand this when Phase 2 adds richer Firestore exception
// handling (quota, server timestamps).
val message = error.message?.lowercase() ?: return SyncError.Unknown(error)
return when {
"unauthenticated" in message || "auth" in message -> SyncError.AuthRequired
"permission" in message -> SyncError.PermissionDenied
"unavailable" in message ||
"network" in message ||
"deadline" in message ||
"timeout" in message -> SyncError.NetworkUnavailable
else -> SyncError.Unknown(error)
}
}