Skip to content

Commit

Permalink
add unit tests for the ActivityResults API usage
Browse files Browse the repository at this point in the history
  • Loading branch information
lbalmaceda committed Oct 27, 2021
1 parent 8ba5b84 commit c458588
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 8 deletions.
2 changes: 1 addition & 1 deletion auth0/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,6 @@ dependencies {
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion"
testImplementation "com.squareup.okhttp3:okhttp-tls:$okhttpVersion"
testImplementation 'com.jayway.awaitility:awaitility:1.7.0'
testImplementation 'org.robolectric:robolectric:4.4'
testImplementation 'org.robolectric:robolectric:4.6.1'
testImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import android.os.Build
import android.text.TextUtils
import android.util.Base64
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.IntRange
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
import com.auth0.android.callback.AuthenticationCallback
Expand Down Expand Up @@ -74,7 +75,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
* The activity passed as first argument here must override the [Activity.onActivityResult] method and
* call [SecureCredentialsManager.checkAuthenticationResult] with the received parameters.
*
* @param activity a valid activity context. Will be used in the authentication request to launch a LockScreen intent.
* @param activity a valid activity context. Will be used in the authentication request to launch a LockScreen intent. If this is a subclass of ComponentActivity, the ActivityResult API will be used.
* @param requestCode the request code to use in the authentication request. Must be a value between 1 and 255.
* @param title the text to use as title in the authentication screen. Passing null will result in using the OS's default value.
* @param description the text to use as description in the authentication screen. On some Android versions it might not be shown. Passing null will result in using the OS's default value.
Expand All @@ -99,11 +100,13 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
&& authIntent != null)
if (authenticateBeforeDecrypt) {
authenticationRequestCode = requestCode
if (activity is AppCompatActivity) {
if (activity is ComponentActivity) {
activityResultContract =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
checkAuthenticationResult(authenticationRequestCode, it.resultCode)
}
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult(),
activity.activityResultRegistry,
fun(it: ActivityResult) {
checkAuthenticationResult(authenticationRequestCode, it.resultCode)
})
} else {
this.activity = activity
}
Expand Down Expand Up @@ -227,7 +230,7 @@ public class SecureCredentialsManager @VisibleForTesting(otherwise = VisibleForT
this.scope = scope
this.minTtl = minTtl
activityResultContract?.launch(authIntent)
?: activity!!.startActivityForResult(authIntent, authenticationRequestCode)
?: activity?.startActivityForResult(authIntent, authenticationRequestCode)
return
}
continueGetCredentials(scope, minTtl, callback)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import android.content.Context
import android.content.Intent
import android.os.Build.VERSION
import android.util.Base64
import androidx.activity.ComponentActivity
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.ActivityResultRegistry
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.app.ActivityOptionsCompat
import com.auth0.android.Auth0
import com.auth0.android.authentication.AuthenticationAPIClient
import com.auth0.android.authentication.AuthenticationException
Expand Down Expand Up @@ -1193,6 +1200,139 @@ public class SecureCredentialsManagerTest {
MatcherAssert.assertThat(willAskAuthentication, Is.`is`(true))
}

@Test
public fun shouldGetCredentialsAfterAuthenticationUsingActivityContracts() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
insertTestCredentials(true, true, false, expiresAt, "scope")
Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at"))
.thenReturn(expiresAt.time)

val kService = mock<KeyguardManager>()
val confirmCredentialsIntent = mock<Intent>()
val contractCaptor = argumentCaptor<ActivityResultContract<Intent, ActivityResult>>()
val callbackCaptor = argumentCaptor<ActivityResultCallback<ActivityResult>>()

val activityController = Robolectric.buildActivity(
ComponentActivity::class.java
).create()
val activity = Mockito.spy(activityController.get())
val successfulResult = ActivityResult(Activity.RESULT_OK, null)
val rRegistry = object : ActivityResultRegistry() {
override fun <I : Any?, O : Any?> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
MatcherAssert.assertThat(input, Is.`is`(confirmCredentialsIntent))
dispatchResult(requestCode, successfulResult)
}
}

Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService)
Mockito.`when`(kService.isKeyguardSecure).thenReturn(true)
Mockito.`when`(activity.activityResultRegistry).thenReturn(rRegistry)
Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription"))
.thenReturn(confirmCredentialsIntent)

//Require authentication
val willRequireAuthentication =
manager.requireAuthentication(activity, 123, "theTitle", "theDescription")
MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true))

Mockito.verify(activity)
.registerForActivityResult(
contractCaptor.capture(),
eq(rRegistry),
callbackCaptor.capture()
)

// Trigger the prompt for credentials and move the activity to "start" so pending ActivityResults are dispatched
activityController.start()
manager.getCredentials(callback)
verify(activity, never()).startActivityForResult(any(), anyInt())

//Continue after successful authentication
verify(callback).onSuccess(
credentialsCaptor.capture()
)
val retrievedCredentials = credentialsCaptor.firstValue
MatcherAssert.assertThat(retrievedCredentials, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.accessToken, Is.`is`("accessToken"))
MatcherAssert.assertThat(retrievedCredentials.idToken, Is.`is`("idToken"))
MatcherAssert.assertThat(retrievedCredentials.refreshToken, Is.`is`(Matchers.nullValue()))
MatcherAssert.assertThat(retrievedCredentials.type, Is.`is`("type"))
MatcherAssert.assertThat(retrievedCredentials.expiresAt, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(retrievedCredentials.expiresAt.time, Is.`is`(expiresAt.time))
MatcherAssert.assertThat(retrievedCredentials.scope, Is.`is`("scope"))

//A second call to (originally called internally) checkAuthenticationResult should fail as callback is set to null
val retryCheck = manager.checkAuthenticationResult(123, Activity.RESULT_OK)
MatcherAssert.assertThat(retryCheck, Is.`is`(false))
}

@Test
public fun shouldNotGetCredentialsAfterCanceledAuthenticationUsingActivityContracts() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
insertTestCredentials(true, true, false, expiresAt, "scope")
Mockito.`when`(storage.retrieveLong("com.auth0.credentials_expires_at"))
.thenReturn(expiresAt.time)

val kService = mock<KeyguardManager>()
val confirmCredentialsIntent = mock<Intent>()
val contractCaptor = argumentCaptor<ActivityResultContract<Intent, ActivityResult>>()
val callbackCaptor = argumentCaptor<ActivityResultCallback<ActivityResult>>()

val activityController = Robolectric.buildActivity(
ComponentActivity::class.java
).create()
val activity = Mockito.spy(activityController.get())
val canceledResult = ActivityResult(Activity.RESULT_CANCELED, null)
val rRegistry = object : ActivityResultRegistry() {
override fun <I : Any?, O : Any?> onLaunch(
requestCode: Int,
contract: ActivityResultContract<I, O>,
input: I,
options: ActivityOptionsCompat?
) {
MatcherAssert.assertThat(input, Is.`is`(confirmCredentialsIntent))
dispatchResult(requestCode, canceledResult)
}
}

Mockito.`when`(activity.getSystemService(Context.KEYGUARD_SERVICE)).thenReturn(kService)
Mockito.`when`(kService.isKeyguardSecure).thenReturn(true)
Mockito.`when`(activity.activityResultRegistry).thenReturn(rRegistry)
Mockito.`when`(kService.createConfirmDeviceCredentialIntent("theTitle", "theDescription"))
.thenReturn(confirmCredentialsIntent)

//Require authentication
val willRequireAuthentication =
manager.requireAuthentication(activity, 123, "theTitle", "theDescription")
MatcherAssert.assertThat(willRequireAuthentication, Is.`is`(true))

Mockito.verify(activity)
.registerForActivityResult(
contractCaptor.capture(),
eq(rRegistry),
callbackCaptor.capture()
)

// Trigger the prompt for credentials and move the activity to "start" so pending ActivityResults are dispatched
activityController.start()
manager.getCredentials(callback)
verify(activity, never()).startActivityForResult(any(), anyInt())
verify(callback, never()).onSuccess(any())
verify(callback).onFailure(
exceptionCaptor.capture()
)
MatcherAssert.assertThat(exceptionCaptor.firstValue, Is.`is`(Matchers.notNullValue()))
MatcherAssert.assertThat(
exceptionCaptor.firstValue.message,
Is.`is`("The user didn't pass the authentication challenge.")
)
}

@Test
public fun shouldGetCredentialsAfterAuthentication() {
val expiresAt = Date(CredentialsMock.ONE_HOUR_AHEAD_MS)
Expand Down

0 comments on commit c458588

Please sign in to comment.