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

Commit

Permalink
Add a lightweight FirefoxAccountsAuthFeature
Browse files Browse the repository at this point in the history
Ties together an account manager with a tabs use cases. Let's see
how it pans out in the reference browser!
  • Loading branch information
Grisha Kruglov authored and grigoryk committed Jan 26, 2019
1 parent f6f4a2a commit e785bb0
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .buildconfig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ projects:
path: components/feature/awesomebar
description: 'Component connecting a concept-toolbar with a concept-awesomebar.'
publish: true
feature-accounts:
path: components/feature/accounts
description: 'Component for tying an account manager with the tabs feature to facilitate auth flows.'
publish: true
feature-contextmenu:
path: components/feature/contextmenu
description: 'Component for displaying context menus for web content.'
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ _API contracts and abstraction layers for browser components._

_Combined components to implement feature-specific use cases._

* 🔴 [**Accounts**](components/feature/accounts/README.md) - A component that connects an FxaAccountManager from [service-firefox-accounts](components/service/firefox-accounts/README.md) with [feature-tabs](components/feature/tabs/README.md) in order to facilitate authentication flows.

*[**Awesomebar**](components/feature/awesomebar/README.md) - A component that connects a [concept-awesomebar](components/concept/awesomebar/README.md) implementation to a [concept-toolbar](components/concept/toolbar/README.md) implementation and provides implementations of various suggestion providers.

*[**Context Menu**](components/feature/contextmenu/README.md) - A component for displaying context menus when *long-pressing* web content.
Expand Down
18 changes: 18 additions & 0 deletions components/feature/accounts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# [Android Components](../../../README.md) > Feature > Accounts

A component which ties together an FxaAccountManager with the tabs feature, to
facilitate OAuth authentication flows managed by the account manager.

### Setting up the dependency

Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):

```Groovy
implementation "org.mozilla.components:feature-accounts:{latest-version}"
```

## License

This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/
39 changes: 39 additions & 0 deletions components/feature/accounts/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

android {
compileSdkVersion config.compileSdkVersion

defaultConfig {
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation Dependencies.kotlin_coroutines

implementation project(':concept-engine')
implementation project(':feature-tabs')
implementation project(':service-firefox-accounts')

testImplementation Dependencies.testing_junit
testImplementation Dependencies.testing_mockito
testImplementation Dependencies.testing_robolectric

testImplementation project(':support-test')
}

apply from: '../../../publish.gradle'
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description)
21 changes: 21 additions & 0 deletions components/feature/accounts/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
5 changes: 5 additions & 0 deletions components/feature/accounts/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="mozilla.components.feature.accounts" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.accounts

import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.request.RequestInterceptor
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.fxa.FxaAccountManager
import mozilla.components.service.fxa.FxaException
import kotlin.coroutines.CoroutineContext

/**
* Ties together an account manager with a session manager/tabs implementation, facilitating an
* authentication flow.
*/
class FirefoxAccountsAuthFeature(
private val accountManager: FxaAccountManager,
private val tabsUseCases: TabsUseCases,
private val redirectUrl: String,
private val successPath: String,
private val coroutineContext: CoroutineContext = Dispatchers.Main
) {
fun beginAuthentication() {
CoroutineScope(coroutineContext).launch {
val authUrl = try {
accountManager.beginAuthentication().await()
} catch (e: FxaException) {
// FIXME return a fallback URL provided by Config...
"https://accounts.firefox.com/signin"
}
// TODO
// We may fail to obtain an authentication URL, for example due to transient network errors.
// If that happens, open up a fallback URL in order to present some kind of a "no network"
// UI to the user.
// It's possible that the underlying problem will go away by the time the tab actually
// loads, resulting in a confusing experience.
tabsUseCases.addTab.invoke(authUrl)
}
}

val interceptor = object : RequestInterceptor {
override fun onLoadRequest(session: EngineSession, uri: String): RequestInterceptor.InterceptionResponse? {
if (!uri.startsWith(redirectUrl)) {
return null
}

val parsedUri = Uri.parse(uri)
val code = parsedUri.getQueryParameter("code") as String
val state = parsedUri.getQueryParameter("state") as String

// Notify the state machine about our success.
accountManager.finishAuthentication(code, state)

// TODO this can be simplified once https://github.com/mozilla/application-services/issues/305 lands
val successUrl = "${parsedUri.scheme}://${parsedUri.host}/$successPath"
return RequestInterceptor.InterceptionResponse.Url(successUrl)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.accounts

import android.content.Context
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.runBlocking
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.fxa.AccountStorage
import mozilla.components.service.fxa.Config
import mozilla.components.service.fxa.FirefoxAccountShaped
import mozilla.components.service.fxa.FxaAccountManager
import mozilla.components.service.fxa.FxaNetworkException
import mozilla.components.service.fxa.Profile
import mozilla.components.service.fxa.SharedPrefAccountStorage
import mozilla.components.support.test.any
import mozilla.components.support.test.mock
import org.junit.Test

import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.Mockito.`when`
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment

// Same as the actual account manager, except we get to control how FirefoxAccountShaped instances
// are created. This is necessary because due to some build issues (native dependencies not available
// within the test environment) we can't use fxaclient supplied implementation of FirefoxAccountShaped.
// Instead, we express all of our account-related operations over an interface.
class TestableFxaAccountManager(
context: Context,
config: Config,
scopes: Array<String>,
accountStorage: AccountStorage = SharedPrefAccountStorage(context),
val block: () -> FirefoxAccountShaped = { mock() }
) : FxaAccountManager(context, config, scopes, accountStorage) {
override fun createAccount(config: Config): FirefoxAccountShaped {
return block()
}
}

@RunWith(RobolectricTestRunner::class)
class FirefoxAccountsAuthFeatureTest {
@Test
fun `begin authentication`() {
val accountStorage = mock<AccountStorage>()
val mockAccount: FirefoxAccountShaped = mock()

val profile = Profile(
uid = "testUID", avatar = null, email = "[email protected]", displayName = "test profile")

Mockito.`when`(mockAccount.getProfile(ArgumentMatchers.anyBoolean())).thenReturn(CompletableDeferred(profile))
Mockito.`when`(mockAccount.beginOAuthFlow(any(), ArgumentMatchers.anyBoolean()))
.thenReturn(CompletableDeferred("auth://url"))
// This ceremony is necessary because CompletableDeferred<Unit>() is created in an _active_ state,
// and threads will deadlock since it'll never be resolved while state machine is waiting for it.
// So we manually complete it here!
val unitDeferred = CompletableDeferred<Unit>()
unitDeferred.complete(Unit)
Mockito.`when`(mockAccount.completeOAuthFlow(
ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
).thenReturn(unitDeferred)
// There's no account at the start.
Mockito.`when`(accountStorage.read()).thenReturn(null)

val manager = TestableFxaAccountManager(
RuntimeEnvironment.application,
Config.release("dummyId", "bad://url"),
arrayOf("profile", "test-scope"),
accountStorage
) {
mockAccount
}

runBlocking {
manager.init().await()
}

val mockAddTab: TabsUseCases.AddNewTabUseCase = mock()
val mockTabs: TabsUseCases = mock()
`when`(mockTabs.addTab).thenReturn(mockAddTab)

runBlocking {
val feature = FirefoxAccountsAuthFeature(
manager, mockTabs, "somePath", "somePath", this.coroutineContext)
feature.beginAuthentication()
}

verify(mockAddTab, times(1)).invoke("auth://url")
Unit
}

@Test
fun `begin authentication with errors`() {
val accountStorage = mock<AccountStorage>()
val mockAccount: FirefoxAccountShaped = mock()

val profile = Profile(
uid = "testUID", avatar = null, email = "[email protected]", displayName = "test profile")

Mockito.`when`(mockAccount.getProfile(ArgumentMatchers.anyBoolean())).thenReturn(CompletableDeferred(profile))

val exceptionalDeferred = CompletableDeferred<String>()
exceptionalDeferred.completeExceptionally(FxaNetworkException("oops"))
Mockito.`when`(mockAccount.beginOAuthFlow(any(), ArgumentMatchers.anyBoolean()))
.thenReturn(exceptionalDeferred)
// This ceremony is necessary because CompletableDeferred<Unit>() is created in an _active_ state,
// and threads will deadlock since it'll never be resolved while state machine is waiting for it.
// So we manually complete it here!
val unitDeferred = CompletableDeferred<Unit>()
unitDeferred.complete(Unit)
Mockito.`when`(mockAccount.completeOAuthFlow(
ArgumentMatchers.anyString(), ArgumentMatchers.anyString())
).thenReturn(unitDeferred)
// There's no account at the start.
Mockito.`when`(accountStorage.read()).thenReturn(null)

val manager = TestableFxaAccountManager(
RuntimeEnvironment.application,
Config.release("dummyId", "bad://url"),
arrayOf("profile", "test-scope"),
accountStorage
) {
mockAccount
}

runBlocking {
manager.init().await()
}

val mockAddTab: TabsUseCases.AddNewTabUseCase = mock()
val mockTabs: TabsUseCases = mock()
`when`(mockTabs.addTab).thenReturn(mockAddTab)

runBlocking {
val feature = FirefoxAccountsAuthFeature(
manager, mockTabs, "somePath", "somePath", this.coroutineContext)
feature.beginAuthentication()
}

// Fallback url is invoked.
verify(mockAddTab, times(1)).invoke("https://accounts.firefox.com/signin")
Unit
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mock-maker-inline
// This allows mocking final classes (classes are final by default in Kotlin)
2 changes: 1 addition & 1 deletion components/feature/tabs/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ android {


dependencies {
implementation project(':browser-session')
api project(':browser-session')
implementation project(':concept-engine')
implementation project(':concept-tabstray')
implementation project(':concept-toolbar')
Expand Down

0 comments on commit e785bb0

Please sign in to comment.