Coverage Summary for Class: DeleteUserAccountUseCase (com.georgeci.moneysurfer.domain.usecase)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| DeleteUserAccountUseCase |
100%
(5/5)
|
62.5%
(5/8)
|
90.3%
(28/31)
|
97.3%
(534/549)
|
| DeleteUserAccountUseCase$invoke$1 |
|
| DeleteUserAccountUseCase$reauthenticate$1 |
|
| DeleteUserAccountUseCase$WhenMappings |
|
| Total |
100%
(5/5)
|
62.5%
(5/8)
|
90.3%
(28/31)
|
97.3%
(534/549)
|
package com.georgeci.moneysurfer.domain.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
import com.georgeci.moneysurfer.domain.auth.AccountDeletionError
import com.georgeci.moneysurfer.domain.auth.AuthError
import com.georgeci.moneysurfer.domain.auth.SessionMutator
import com.georgeci.moneysurfer.domain.repositories.AuthRemoteRepository
import com.georgeci.moneysurfer.domain.repositories.LocalDataResetRepository
import com.georgeci.moneysurfer.domain.repositories.RemoteDataResetRepository
import com.georgeci.moneysurfer.domain.repositories.SessionShutdownGate
import com.georgeci.moneysurfer.domain.repositories.UserAccountDeletionRepository
import org.koin.core.annotation.Single
/**
* Permanently deletes the signed-in user: their Firestore footprint, the Firebase Auth user,
* and every trace on this device (issue #213; semantics match the hosted account-deletion page).
*
* Step order is load-bearing:
* 1. re-auth first, so a wrong password aborts before anything is touched;
* 2. sync shutdown before remote cleanup, so an in-flight push can't recreate purged docs;
* 3. Firestore cleanup while the auth user still exists — rules authorize each delete
* against `request.auth.uid`;
* 4. auth-user deletion is the point of no return;
* 5. local teardown last, mirroring [LogoutUseCase] — clearing `currentUserId` is also what
* bounces navigation to SignIn (see AppLaunchViewModel).
*
* On failure the local session is intentionally left intact so the user can retry; every
* remote step is idempotent.
*/
@Single
class DeleteUserAccountUseCase(
private val sessionShutdownGate: SessionShutdownGate,
private val authRemoteRepository: AuthRemoteRepository,
private val userAccountDeletionRepository: UserAccountDeletionRepository,
private val localDataResetRepository: LocalDataResetRepository,
private val remoteDataResetRepository: RemoteDataResetRepository,
private val sessionMutator: SessionMutator,
) {
/**
* [password] re-authenticates email/password sessions; pass null for anonymous sessions
* (they cannot re-auth — a stale one surfaces [AccountDeletionError.RequiresRecentLogin]).
*/
suspend operator fun invoke(password: String?): Either<AccountDeletionError, Unit> = either {
val uid = ensureNotNull(authRemoteRepository.currentUid()) { AccountDeletionError.NotSignedIn }
if (password != null) reauthenticate(password).bind()
sessionShutdownGate.shutdown()
userAccountDeletionRepository
.deleteRemoteUserData(uid = uid, email = authRemoteRepository.currentEmail())
.bind()
authRemoteRepository.deleteCurrentUser()
.mapLeft { it.toDeleteError() }
.bind()
sessionMutator.clearSession()
localDataResetRepository.clearAll()
remoteDataResetRepository.clearAll()
}
private suspend fun reauthenticate(password: String): Either<AccountDeletionError, Unit> {
val email = authRemoteRepository.currentEmail()
?: return AccountDeletionError.NotSignedIn.left()
return authRemoteRepository.reauthenticateWithEmail(email, password)
.mapLeft { it.toReauthError() }
}
private fun AuthError.toReauthError(): AccountDeletionError = when (type) {
AuthError.Type.InvalidCredentials -> AccountDeletionError.InvalidCredentials
else -> AccountDeletionError.ReauthenticationFailed(cause = cause)
}
private fun AuthError.toDeleteError(): AccountDeletionError = when (type) {
AuthError.Type.RequiresRecentLogin -> AccountDeletionError.RequiresRecentLogin
else -> AccountDeletionError.AuthDeletionFailed(cause = cause)
}
}