Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

For #12289 Add lib-auth for authentication using biometrics or PIN. #12291

Merged
merged 13 commits into from
Jun 23, 2022
Merged

Conversation

iorgamgabriel
Copy link
Contributor

@iorgamgabriel iorgamgabriel commented Jun 6, 2022

Pull Request checklist

  • Quality: This PR builds and passes detekt/ktlint checks (A pre-push hook is recommended)
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry or does not need one
  • Accessibility: The code in this PR follows accessibility best practices or does not include any user facing features

After merge

  • Milestone: Make sure issues closed by this pull request are added to the milestone of the version currently in development.
  • Breaking Changes: If this is a breaking change, please push a draft PR on Reference Browser to address the breaking issues.

@iorgamgabriel iorgamgabriel requested review from a team as code owners June 6, 2022 13:13
@iorgamgabriel iorgamgabriel requested review from escapewindow and removed request for a team June 6, 2022 13:13
@iorgamgabriel iorgamgabriel requested a review from jonalmeida June 6, 2022 13:26
@iorgamgabriel iorgamgabriel changed the title For #12289 Implement the common part from Biometric in ac For #12289 Add lib-auth for authentication using biometrics or PIN. Jun 8, 2022
@escapewindow escapewindow removed their request for review June 8, 2022 19:48
@escapewindow
Copy link
Contributor

The taskcluster.ci.config change looks reasonable. I'll leave the actual review to people who understand the rest of the patch :)

.buildconfig.yml Outdated Show resolved Hide resolved
@iorgamgabriel iorgamgabriel force-pushed the 12289 branch 2 times, most recently from 7073bef to 6bde86a Compare June 10, 2022 06:28
@iorgamgabriel iorgamgabriel requested a review from jonalmeida June 10, 2022 07:58
Copy link
Contributor

@jonalmeida jonalmeida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! A few more nits to fix and I think we are almost there to land this.

/**
* Checks if the appropriate SDK version and hardware capabilities are met to use the feature.
*/
fun canUseFeature(context: Context): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we only want this method, can we just make this one an extension function so that we don't have to expose a utils class in the component for only this one?

We try to avoid util classes because they have potential to be a dumping ground for functions that never end up getting used anywhere else. With extension functions, they are more likely to be used (in my biased experience) since the autocomplete for the IDE lists them out as an option.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do this I can't mock with Mockito extension functions . I need to use mockk . @jonalmeida

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replied back to this thread in another one here: #12291 (comment)

@iorgamgabriel iorgamgabriel force-pushed the 12289 branch 4 times, most recently from f367f5c to 75f58f1 Compare June 14, 2022 11:16
@mergify
Copy link
Contributor

mergify bot commented Jun 14, 2022

This pull request has conflicts when rebasing. Could you fix it @iorgamgabriel? 🙏

@iorgamgabriel iorgamgabriel requested a review from jonalmeida June 16, 2022 07:15
@@ -85,6 +86,7 @@ object Dependencies {
const val testing_robolectric = "org.robolectric:robolectric:${Versions.robolectric}"
const val testing_robolectric_playservices = "org.robolectric:shadows-playservices:${Versions.robolectric}"
const val testing_mockito = "org.mockito:mockito-core:${Versions.mockito}"
const val testing_mockito_inline = "org.mockito:mockito-inline:${Versions.mockito_inline}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any usages for this dependency in the tests. Can we remove it if it's not needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


@Config(sdk = [M])
@Test
fun `GIVEN canUseFeature is called WHEN hardware is available and biometric is enrolled THEN canUseFeature return true`() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Separate test file for each class file that we have.

BiometricPromptAuth -> BiometricPromptAuthTest
BiometricUtils -> BiometricUtilsTest

etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

/**
* Utility class for BiometricPromptAuth
*/
class BiometricUtils {
Copy link
Contributor

@jonalmeida jonalmeida Jun 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replying back to #12291 (comment) over here as well:

Ah, I didn't catch this earlier. I see that you making the utils class an instance for testing. Consider now though, that if you want to use canUseFeature in your application code, you now have to create an instance of this class every time.

// Expanded into two lines for clarity.
val utils = BiometricUtils()
utils.canUseFeature(context)

// A better API would not need an instance if it was static.
BiometricUtils.canUseFeature(context)

So we've made testing "easier" but it's at the cost of bad design pattern. You have tests for isHardwareAvailable and isEnrolled which is where our logic is and the things we should be testing. The last bit of logic that you would want to test is the bit in canUseFeature:

return isHardwareAvailable(manager) && isEnrolled(manager)

We can write this code in a way so that we pass the manager to it in an internal function (that can be mocked and tested just like you did for the other methods, then leave the Android platform code as a wrapper. What that would look like:

// Our public API for the extension function. Only test to write is for the SDK_INT case when it's false.
fun Context.canUseBiometricFeature(): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        val manager = BiometricManager.from(this)
        return BiometricUtils.canUseFeature(manager)
    } else {
        false
    }
}

// note that this is an object and not a class.
internal object BiometricUtils {

    // Keep our logic in a static function that takes in it's dependencies (here, it's the BiometricManager) as a parameter which we can mock)
    internal fun canUseFeature(manager: BiometricManager): Boolean {
        return isHardwareAvailable(manager) && isEnrolled(manager)
    }

    internal fun isHardwareAvailable(manager: BiometricManager): Boolean {}

    internal fun isEnrolled(manager: BiometricManager): Boolean {}
}

Let me know if that makes sense or not. Happy to talk about it more. 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Contributor

@jonalmeida jonalmeida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one last fix to add the tests and we should be good to land this.

/**
* Checks if the hardware requirements are met for using the [BiometricManager].
*/
private fun isHardwareAvailable(biometricManager: BiometricManager): Boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, maybe I wasn't clear: you can still write tests for these easily I think, but you don't have to make the utils class public.

So the methods can be internal:

    internal fun isHardwareAvailable(biometricManager: BiometricManager): Boolean { }
    internal fun isEnrolled(biometricManager: BiometricManager): Boolean { }

And the tests can take a mocked biometric manager to write the tests for those methods:

    @Test
    fun `isHardwareAvailable is true based on AuthenticationStatus`() {
        val manager: BiometricManager = mock {
            whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
                .thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
                .thenReturn(BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE)
                .thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE)
        }

        assertTrue(BiometricUtils.isHardwareAvailable(manager))
        assertFalse(BiometricUtils.isHardwareAvailable(manager))
        assertFalse(BiometricUtils.isHardwareAvailable(manager))
    }

    @Test
    fun `isEnrolled is true based on AuthenticationStatus`() {
        val manager: BiometricManager = mock {
            whenever(canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
                .thenReturn(BiometricManager.BIOMETRIC_SUCCESS)
        }
        assertTrue(BiometricUtils.isEnrolled(manager))
    }

The test you added for canUseFeature is good enough coverage for that method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@iorgamgabriel iorgamgabriel requested a review from jonalmeida June 23, 2022 08:36
Copy link
Contributor

@jonalmeida jonalmeida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to go!

Related: I noticed when reviewing this PR, that there very similar code in mozilla.components.feature.autofill.authenticator that has the same flow. I think we should eventually replace that code with what you've built here in a future issue. I've filed #12379 if you're interested in also picked that up.

Since you have multiple commits, we should really make this into one commit before landing. For this PR, I can use "needs landing (squash)" instead.

@jonalmeida jonalmeida added the 🛬 needs landing (squash) PRs that are ready to land (squashed) label Jun 23, 2022
@jonalmeida jonalmeida linked an issue Jun 23, 2022 that may be closed by this pull request
@mergify mergify bot merged commit b172382 into main Jun 23, 2022
@bors bors bot deleted the 12289 branch June 23, 2022 18:28
@iorgamgabriel
Copy link
Contributor Author

@jonalmeida Thank you ! I will take #12379

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
🛬 needs landing (squash) PRs that are ready to land (squashed)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add lib-auth for authentication using biometrics or PIN.
6 participants