Kotlin Callback unit test - android

I'm writing a unit test for my app. I need to test a function that handles the login inside my app. This login handler uses a Callback that notifies when the login is complete. This is my Login Handler
class LoginManager {
private var auth: FirebaseAuth = Firebase.auth
fun loginWithEmail(
email: String,
password: String,
loginCallback: LoginCallback
) {
auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { loginTask ->
if (loginTask.isSuccessful) {
auth.currentUser?.let { user ->
loginCallback.onLoginSuccess(user)
return#addOnCompleteListener
}
}
loginCallback.onLoginFailure(loginTask.exception)
}
} }
LoginCallback is a simple interface
interface LoginCallback {
fun onLoginSuccess(user: FirebaseUser)
fun onLoginFailure(error: Exception?) }
I need to test the loginWithEmail function. In my test I try this code
Handler(getMainLooper()).post {
var firebaseUser: FirebaseUser? = null
val loginCallback = object : LoginCallback {
override fun onLoginSuccess(user: FirebaseUser) {
firebaseUser = user
}
override fun onLoginFailure(error: Exception?) {
//No action needed
}
}
val loginManager = LoginManager(loginCallback)
loginManager.loginWithEmail(EXISTING_EMAIL_ADDRESS, CORRECT_PASSWORD)
Thread.sleep(5000)
assert(firebaseUser != null)
}
shadowOf(getMainLooper()).idle()
But the callback is never being called. I also try with an ArgumentCaptor
Handler(getMainLooper()).post {
val captor = ArgumentCaptor.forClass(LoginCallback::class.java)
val loginManager = LoginManager()
loginManager.loginWithEmail(EXISTING_EMAIL_ADDRESS, CORRECT_PASSWORD, captor.capture())
}
shadowOf(getMainLooper()).idle()
But with this code, I'm getting this error java.lang.NullPointerException: captor.capture() must not be null
Is there a way to test my function?

Related

How to replace callbacks in coroutines android

here is my code, I am using it for logging in user with google,
This is my viewModel code
fun signInWithGoogle(account: GoogleSignInAccount): LiveData<Resource<Any>> {
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
return liveData (IO){
authRepo.firebaseSignInWithGoogle(credential, object : FetchUser {
override suspend fun onUserDataFetch(user: User) {
this#liveData.emit(Resource.success(user))
}
override suspend fun onError(error: AppError?) {
this#liveData.emit(Resource.error(error, null))
}
})
}
}
This is my code authRepository where i am logging the user in and checking if user already exits in database or not according to that performing the work
suspend fun firebaseSignInWithGoogle(googleAuthCredential: AuthCredential, userCallBack: FetchUser) {
coroutineScope {
firebaseAuth.signInWithCredential(googleAuthCredential).await()
createUpdateUser(userCallBack)
}
}
private suspend fun createUpdateUser(userCallBack: FetchUser) {
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser != null) {
userIntegrator.getUserById(firebaseUser.uid, object : OnDataChanged {
override suspend fun onChanged(any: Any?) {
if (any != null && any is User) {
any.isNew = false
userIntegrator.createUpdateUser(any, userCallBack)
} else {
val user = User()
user.id = firebaseUser.uid
user.name = firebaseUser.displayName
user.email = firebaseUser.email
user.isNew = true
userIntegrator.createUpdateUser(
user,
userCallBack
)
}
}
})
}
}
This is my last class where I am updating the user in database
suspend fun createUpdateUser(user: User, userCallBack: FetchUser) {
if (user.id.isNullOrEmpty()) {
userCallBack.onError(AppError(StatusCode.UnSuccess, ""))
return
}
val dp = databaseHelper.dataFirestoreReference?.collection(DatabaseHelper.USERS)?.document()
dp?.apply {
dp.set(user.toMap()).await().apply {
dp.get().await().toObject(User::class.java)?.let {
userCallBack.onUserDataFetch(it)
}?: kotlin.run {
userCallBack.onError(AppError(StatusCode.Exception,"Unable to add user at the moment"))
}
}
}
}
Now here whole thing is that, I am using a FetchUser interface which look like this
interface FetchUser {
suspend fun onUserDataFetch(user: User)
suspend fun onError(error: AppError?)
}
I just want to get rid of it and looking for something else in coroutines.
Also I just wanted to know the best practice here,
What should I do with it.
Also I want to make it unit testable
There are 2 ways, if you want to call and get result directly, you could use suspendCoroutine. Otherway, if you want to get stream of data like, loading, result, error,... you could try callbackFlow
Exp:
suspend fun yourMethod() = suspendCoroutine { cont ->
// do something
cont.resume(result)
}
suspend fun yourMethod() = callbackFlow {
val callbackImpl = object: yourInterace {
// your implementation
fun onSuccess() {
emit(your result)
}
fun onFailed() {
emit(error)
}
}
handleYourcallback(callbackImpl)
}

FirebaseAuth - how can I wait for value

I use FirebaseAuth for registration new user
class FirebaseAuthenticationServiceImpl(): FirebaseAuthenticationService {
override fun registerWithEmailAndPassword(email: String, password: String): Boolean {
val registration = FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
println(it.additionalUserInfo?.providerId.toString())
}.addOnFailureListener {
println(it.message.toString())
}
return registration.isSuccessful
}
}
I call function above and every time I get false. After some time I get true
coroutineScope {
try {
if (firebaseService.registerWithEmailAndPassword(email, password)) {
openHomeActivity.offer(Unit)
} else {}
} catch (e: Exception) {}
}
How can I wait for uth result (success/failure) and afer that get that value?
Where is FirebaseAuthenticationService from? Do you need it? The official getting started guide just uses Firebase.auth. With this, you can authenticate using the await() suspend function instead of using the callback approach.
// In a coroutine:
val authResult = Firebase.auth.registerWithEmailAndPassword(email, password).await()
val user: FirebaseUser = authResult.user
if (user != null) {
openHomeActivity.offer(Unit)
} else {
// authentication failed
}
If you are using coroutines you can use suspendCoroutine which is perfect bridge between traditional callbacks and coroutines as it gives you access to the Continuation<T> object, example with a convenience extension function for Task<R> objects :
scope.launch {
val registrationResult = suspendCoroutine { cont -> cont.suspendTask(FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)

FirebaseUser cannot reauthenitcate to delete account

As the title suggests I am having trouble deleting a Firebase user. I have 2 sign in types enabled in Firebase console :
Anonymous
Google
These providers types are mirrored in the application and signing in is not an issue using firebase-ui-auth
listOf(
IdpConfig.AnonymousBuilder().build(),
IdpConfig.GoogleBuilder().build())
I want the user to be able to delete their account, this works fine for Anonymous users but fails for Users that signed in with a google account using a GoogleAuthCredential. In order to do this the documentation states you need to "reauthenticate" : FirebaseUser::reauthenticate. This is where I am having trouble and re-authentication always fails with :
FirebaseAuthInvalidCredentialsException
ERROR_INVALID_CREDENTIAL
The supplied auth credential is malformed or has expired. [ Invalid id_token in IdP response: <token provided in request>, error: id token is not issued by Google. ]
I have checked the token is within the UTC expiry time, and my device clock is set correctly.
Current Code (using coroutines):
class UserActions internal constructor(
private val context: Context,
private val authUI: AuthUI,
private val auth: FirebaseAuth) {
suspend fun signOut(): Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.signOut(context)) }
suspend fun delete(): Boolean {
auth.currentUser
?.takeIf { user -> !user.isAnonymous }
?.let { user ->
val tokenResult: GetTokenResult = suspendCoroutine { cont -> cont.suspendTask(user.getIdToken(true)) }
val credential : AuthCredential = GoogleAuthProvider.getCredential(tokenResult.token, null)
// Point of failure - always returns false with above error.
val success: Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(user.reauthenticate(credential)) }
if (!success) return false
}
return suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.delete(context)) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun Continuation<Boolean>.suspendCompletableTask(task: Task<Void>) {
task.addOnSuccessListener { this.success() }
.addOnFailureListener { this.failure() }
}
private fun Continuation<Boolean>.success() = resume(true)
private fun Continuation<Boolean>.failure() = resume(false)
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)
}
I thought that maybe I had the token incorrectly added as a parameter arguemnt for :
GoogleAuthProvider.getCredential(tokenResult.token, null)
So swapped to :
GoogleAuthProvider.getCredential(null, tokenResult.token)
But said I had an invalid value in the error description so I have the argument correct for the AuthCredential and a "valid" id token as far as I can see.
What am I doing wrong here?
Solved : Simple answer to this one in the end.
Both FirebaseUser and GoogleSigInAccount refer to having a idToken. I was using the former for the token here :
val tokenResult: GetTokenResult = suspendCoroutine { cont -> cont.suspendTask(user.getIdToken(true)) }
val credential : AuthCredential = GoogleAuthProvider.getCredential(tokenResult.token, null)
The AuthCredential was now using the incorrect token. What I should have used was :
val token = GoogleSignIn.getLastSignedInAccount(context)?.idToken.orEmpty()
val credential : AuthCredential = GoogleAuthProvider.getCredential(token, null)
So the error was correct - I was using the FirebaseUser token when reauthenticating a GoogleAuthCredential which should be using the GoogleSigInAccount token.
Update
As tokens are short lived the above can still fail if the token becomes stale. the solution is to perform a "silent sign in" to refresh the token. This cannot be done through FirebaseAuth::silentSignin as this fails if a FirebaseUser is already signed in. It requires a call to GoogleSignInClient::silentSignIn.
Revised full code :
class UserActions internal constructor(
private val context: Context,
private val authUI: AuthUI,
private val auth: FirebaseAuth) {
suspend fun signOut(): Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.signOut(context)) }
suspend fun delete(): Boolean {
auth.currentUser
?.takeIf { user -> !user.isAnonymous }
?.let { user ->
val credential: AuthCredential = GoogleAuthProvider.getCredential(getFreshGoogleIdToken(), null)
val success: Boolean = suspendCoroutine { cont -> cont.suspendCompletableTask(user.reauthenticate(credential)) }
if (!success) return false
}
return suspendCoroutine { cont -> cont.suspendCompletableTask(authUI.delete(context)) }
}
private suspend fun getFreshGoogleIdToken(): String = suspendCoroutine<GoogleSignInAccount> { cont ->
cont.suspendTask(
GoogleSignIn.getClient(
context,
GoogleSignInOptions.Builder()
.requestProfile()
.requestId()
.requestIdToken(context.getString(R.string.default_web_client_id))
.build())
.silentSignIn())
}.idToken.orEmpty()
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun Continuation<Boolean>.suspendCompletableTask(task: Task<Void>) {
task.addOnSuccessListener { this.success() }
.addOnFailureListener { this.failure() }
}
private fun Continuation<Boolean>.success() = resume(true)
private fun Continuation<Boolean>.failure() = resume(false)
private fun <R> Continuation<R>.success(r: R) = resume(r)
private fun <R> Continuation<R>.failure(t: Exception) = resumeWithException(t)
}

How to handle Flow Coroutines Asynchronous Behaviour while using API

I am trying to get The liveStatus of authStateListener using Flow Coroutines .But everytime it returns False. Below is the code with which I tried to implement the following.It follows the MVVM pattern.
Code ->
FirebaseUserFlow
open class FirebaseUserFlow() {
private val firebaseAuth = FirebaseAuth.getInstance()
private var auth: FirebaseUser? = null
#ExperimentalCoroutinesApi
fun getUserInfo(): Flow<FirebaseUser?> =
callbackFlow {
val authStateListener = FirebaseAuth.AuthStateListener {
auth = it.currentUser
}
offer(auth)
firebaseAuth.addAuthStateListener(authStateListener)
awaitClose {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
}
ViewModel
class AuthViewModel : ViewModel() {
enum class AuthenticationClass {
AUTHENTICATED,
UNAUTHENTICATED
}
#ExperimentalCoroutinesApi
val authenticationState = FirebaseUserFlow().getUserInfo().map {
Log.d("Tag","The value of the user is $it")
if (it != null) {
AuthenticationClass.AUTHENTICATED
} else {
AuthenticationClass.UNAUTHENTICATED
}
}.asLiveData()
}
The log above always returns false
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.authenticationState.observe(viewLifecycleOwner, Observer {authenticationstate ->
when (authenticationstate) {
AuthViewModel.AuthenticationClass.AUTHENTICATED -> {
findNavController().navigate(R.id.action_loginFragmentUser_to_homeFragment)
Log.d("TAG","Authenticated")
}
else -> Log.d("TAG","Else")
}
})
}
In the above fragment , In the onActivityCreated the liveData is observed and based on the state it navigates to the Home Fragment .
Your error is here
FirebaseAuth.AuthStateListener {
auth = it.currentUser
}
trySendBlocking(auth)
You should call offer() inside the callback.
open class FirebaseUserFlow() {
private val firebaseAuth = FirebaseAuth.getInstance()
#ExperimentalCoroutinesApi
fun getUserInfo(): Flow<FirebaseUser?> =
callbackFlow {
val authStateListener = FirebaseAuth.AuthStateListener {
trySendBlocking(it.currentUser)
}
firebaseAuth.addAuthStateListener(authStateListener)
awaitClose {
firebaseAuth.removeAuthStateListener(authStateListener)
}
}
}

Coroutin To Perform task

hi this is my user repository
class UserRepository(private val appAuth: FirebaseAuth) : SafeAuthRequest(){
suspend fun userLogin(email: String,password: String) : AuthResult{
return authRequest { appAuth.signInWithEmailAndPassword(email,password)}
}
}
this is the SafeAuthRequest class
open class SafeAuthRequest {
suspend fun<T: Any> authRequest(call : suspend () -> Task<T>) : T{
val task = call.invoke()
if(task.isSuccessful){
return task.result!!
}
else{
val error = task.exception?.message
throw AuthExceptions("$error\nInvalid email or password")
}
}
}
calling above things like that
/** Method to perform login operation with custom */
fun onClickCustomLogin(view: View){
authListener?.onStarted()
Coroutines.main {
try {
val authResult = repository.userLogin(email!!,password!!)
authListener?.onSuccess()
}catch (e : AuthExceptions){
authListener?.onFailure(e.message!!)
}
}
}
and my authListener like this
interface AuthListener {
fun onStarted()
fun onSuccess()
fun onFailure(message: String)
}
I am getting an error as the task is not completed
is the correct way to implement the task
I'm using MVVM architectural pattern, so the example I'm going to provide is called from my ViewModel class, that means I have access to viewModelScope. If you want to run a similar code on Activity class, you have to use the Coroutines scope available for your Activity, for example:
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {...}
Answering your question, what I've done to retrieve login from user repository is this:
//UserRepository.kt
class UserRepository(private val appAuth: FirebaseAuth) {
suspend fun userLogin(email: String, password: String) : LoginResult{
val firebaseUser = appAuth.signInWithEmailAndPassword(email, password).await() // Do not forget .await()
return LoginResult(firebaseUser)
}
}
LoginResult is a wrapper class of firebase auth response.
//ClassViewModel.kt
class LoginFirebaseViewModel(): ViewModel(){
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login() {
viewModelScope.launch {
try {
repository.userLogin(email!!,password!!).let {
_loginResult.value = it
}
} catch (e: FirebaseAuthException) {
// Do something on firebase exception
}
}
}
}
The code on Activity class would be like this:
// Function inside Activity
fun onClickCustomLogin(view: View){
val uiScope = CoroutineScope(Dispatchers.Main)
uiScope.launch {
try {
repository.userLogin(email!!,password!!).let {
authResult = it
}
}catch (e : FirebaseAuthException){
// Do something on firebase exception
}
}
}
One of the main benefits of using Coroutines is that you convert asynchronous code in sequential one. That means you don't need listeners or callbacks.
I hope this help you

Categories

Resources