Skip to content

Commit

Permalink
Authentication flow (#28)
Browse files Browse the repository at this point in the history
* Modify configuration files to support Kotlin & Jetpack Compose

As discussed this morning, we plan on moving to Kotlin and Jetpack compose. This commit marks the beginning of the modification to make it.

* Update minSdk to comply with firebase requirement

Changes:
* Change from minSdk:24 to minSdk:28 to comply with the firebase
  realtime database requirements

* Delete old java activity file and write basic test

Changes:
* Remove `GreetingActivity.java` and `MainActivity.java` and
  dependencies
* Write simple `ComposeActvitityTest.kt`

* Authentication

This commit adds firebase, firebase auth, auth ui and also new interfaces for users and authenticator along with some tests.
FirebaseAuthenticator#authenticate should be tested in a valid Android environment with a login activity.

* Better test

* Compose migration (#26)

* Modify configuration files to support Kotlin & Jetpack Compose

As discussed this morning, we plan on moving to Kotlin and Jetpack compose. This commit marks the beginning of the modification to make it.

* Update minSdk to comply with firebase requirement

Changes:
* Change from minSdk:24 to minSdk:28 to comply with the firebase
  realtime database requirements

* Delete old java activity file and write basic test

Changes:
* Remove `GreetingActivity.java` and `MainActivity.java` and
  dependencies
* Write simple `ComposeActvitityTest.kt`

* fix: Support for coverage with Kotlin

* Added Mockito

* Fixed the build.gradle with proper versions

* 🔥 💯 :triump: base 😡

---------

Co-authored-by: BoyeGuillaume <guillaume.boye@epfl.ch>

* Basic interface

* Theme and basic navigation

* Add login flow

* Try of testing navigation

* Add navigation quick test

* Move navigation into appropriate package and refactor

* Begin of LoginActivityTest

* Add test for LoginActivity

* LoginActivity.kt refactor

* fix: authenticate of FirebaseAuthenticator does not abort authenticate when logged-in

* Navigation test

* Delete a line in LoginScreen (cc) and adds seed for theme

* Delete a line in LoginScreen (cc) again

* Add missing UI test dependency in build.gradle

* Should be ok to merge with master

* Should be ok to merge with master

---------

Co-authored-by: BoyeGuillaume <guillaume.boye@epfl.ch>
  • Loading branch information
Maeeen and BoyeGuillaume authored Mar 16, 2023
1 parent f8d80dd commit 8d1b6a0
Show file tree
Hide file tree
Showing 21 changed files with 863 additions and 90 deletions.
14 changes: 13 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,16 @@ dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
// required if you want to use Mockito for unit tests
testImplementation 'org.mockito:mockito-inline:4.8.0'
testImplementation "org.mockito.kotlin:mockito-kotlin:4.1.0"
testImplementation "org.mockito:mockito-inline:4.8.0"
testImplementation "org.powermock:powermock-core:1.7.3"
testImplementation "org.powermock:powermock-module-junit4:1.7.3"
testImplementation "org.powermock:powermock-api-mockito2:1.7.3"
// required if you want to use Mockito for Android tests
androidTestImplementation 'org.mockito:mockito-android:4.8.0'

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
Expand All @@ -109,6 +112,15 @@ dependencies {
androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"

// Navigation
implementation 'androidx.navigation:navigation-compose:2.5.3'
implementation 'androidx.navigation:navigation-runtime-ktx:2.5.3'
implementation 'androidx.navigation:navigation-testing:2.5.3'

// Authentication
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.firebaseui:firebase-ui-auth:7.2.0'
}

tasks.withType(Test) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.github.geohunt.app.ui

import android.graphics.Bitmap
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.core.app.launchActivity
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.assertNoUnverifiedIntents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.geohunt.app.LoginActivity
import com.github.geohunt.app.MainActivity
import com.github.geohunt.app.authentication.Authenticator
import com.github.geohunt.app.mocks.MockLazyRef
import com.github.geohunt.app.model.LazyRef
import com.github.geohunt.app.model.database.api.Challenge
import com.github.geohunt.app.model.database.api.User
import org.hamcrest.Matchers.*
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CompletableFuture

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@get:Rule val composeTestRule = createAndroidComposeRule<MainActivity>()

@Test
fun opensHomeActivityWhenLoggedIn() {
Authenticator.authInstance.set(MockAuthenticator(MockUser("hello")))

Intents.init()
launchActivity<LoginActivity>()
intended(allOf(hasComponent(MainActivity::class.java.name)))
Intents.release()
}

@Test
fun doesNothingIfNotSignedIn() {
Authenticator.authInstance.set(MockAuthenticator(null))
Intents.init()

launchActivity<LoginActivity>()
intended(allOf(hasComponent(LoginActivity::class.java.name)))
assertNoUnverifiedIntents()
Intents.release()
}

@Test
fun titleOfAppIsShown() {
Authenticator.authInstance.set(MockAuthenticator(null))
launchActivity<LoginActivity>()
composeTestRule.onNodeWithText("GeoHunt").assertExists("Title of app does not appear on log in")
}

@Test
fun clickingOnButtonTriggersSignIn() {
val cf = CompletableFuture<Void>()
Authenticator.authInstance.set(MockAuthenticator(null) {
cf.complete(null)
return@MockAuthenticator CompletableFuture.completedFuture(null)
})
Intents.init()

launchActivity<LoginActivity>()
composeTestRule.onNode(hasTestTag("signin-btn")).performClick()
intended(allOf(
hasComponent(LoginActivity::class.java.name),
hasExtra("login", any(Any::class.java))))
Intents.release()
assert(cf.isDone)
}

class MockUser(
override var displayName: String? = null,
override val uid: String = "1",
override val profilePicture: LazyRef<Bitmap> = MockLazyRef("1") { TODO() },
override val challenges: List<LazyRef<Challenge>> = emptyList(),
override val hunts: List<LazyRef<Challenge>> = emptyList(),
override var score: Number = 1
) : User

class MockAuthenticator(override val user: User?,
val authenticateCb: (a: ComponentActivity) -> CompletableFuture<User> = {
CompletableFuture.completedFuture(null)
}) : Authenticator {
override fun authenticate(activity: ComponentActivity): CompletableFuture<User> {
return authenticateCb(activity)
}

override fun signOut(activity: ComponentActivity): CompletableFuture<Void> {
TODO("Not yet implemented")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.geohunt.app.components.navigation

import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.performClick
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import com.github.geohunt.app.ui.components.navigation.NavigationBar
import com.github.geohunt.app.ui.components.navigation.NavigationController
import com.github.geohunt.app.ui.components.navigation.Route
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class TestNavigation {

@get:Rule
val composeTestRule = createComposeRule()

private lateinit var navController: TestNavHostController

@Before
fun setUp() {
composeTestRule.setContent {
navController = TestNavHostController(LocalContext.current)
navController.navigatorProvider.addNavigator(ComposeNavigator())
NavigationController(navController = navController)
NavigationBar(navController = navController)
}
}

@Test
fun startRouteIsHome() {
assert(Route.Home.route == navController.currentBackStackEntry?.destination?.route)
}

@Test
fun clickingOnButtonSelectsIt() {
for (route in Route.values()) {
val node = composeTestRule.onNode(hasTestTag("navbtn-" + route.route))
node.performClick()
node.assertIsSelected()
}
}

@Test
fun clickingOnButtonRedirects() {
for (route in Route.values()) {
val node = composeTestRule.onNode(hasTestTag("navbtn-" + route.route))
node.performClick()
assert(navController.currentBackStackEntry?.destination?.route == route.route)
}
}
}
11 changes: 8 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.GeoHunt"
android:theme="@style/GeoHunt"
tools:targetApi="31">

<provider
Expand All @@ -34,10 +34,15 @@
</provider>

<activity
android:name=".ComposeActivity"
android:name=".LoginActivity"
android:exported="false"
android:label="@string/title_activity_login"
android:theme="@style/GeoHunt" />
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/title_activity_compose"
android:theme="@style/Theme.GeoHunt">
android:theme="@style/GeoHunt">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down
88 changes: 88 additions & 0 deletions app/src/main/java/com/github/geohunt/app/LoginActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.github.geohunt.app

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.github.geohunt.app.authentication.Authenticator
import com.github.geohunt.app.ui.theme.GeoHuntTheme
import com.github.geohunt.app.ui.theme.md_theme_light_primary
import com.github.geohunt.app.ui.theme.seed

class LoginActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val authenticator: Authenticator = Authenticator.authInstance.get()

authenticator.user?.let { loggedIn() }

if (intent.hasExtra("login")) {
authenticator.authenticate(this@LoginActivity).thenAccept {
it?.let { loggedIn() }
}
}

setContent {
GeoHuntTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LoginScreen(context = this@LoginActivity)
}
}
}
}

private fun loggedIn() {
startActivity(
Intent(this@LoginActivity, MainActivity::class.java)
)
}
}

@OptIn(ExperimentalTextApi::class)
@Composable
fun LoginScreen(context: Context) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(id = R.string.app_name),
textAlign = TextAlign.Center,
style = TextStyle(
brush = Brush.linearGradient(listOf(md_theme_light_primary, seed))
)
)

Spacer(modifier = Modifier.height(20.dp))

Button(modifier = Modifier.testTag("signin-btn"), onClick = {
val intent = Intent(context, LoginActivity::class.java)
intent.putExtra("login", 1)
context.startActivity(intent)
}) {
Text(stringResource(id = R.string.sign_in))
}
}
}
49 changes: 49 additions & 0 deletions app/src/main/java/com/github/geohunt/app/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.geohunt.app

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.Dp
import androidx.navigation.compose.rememberNavController
import com.github.geohunt.app.ui.components.navigation.NavigationBar
import com.github.geohunt.app.ui.components.navigation.NavigationController
import com.github.geohunt.app.ui.theme.GeoHuntTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GeoHuntTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainComposable()
}
}
}
}
}

@Composable
fun MainComposable() {
val navController = rememberNavController()
Scaffold(
bottomBar = {
Surface(modifier = Modifier.shadow(Dp(9f))) {
NavigationBar(navController = navController)
}
}
) { padding ->
NavigationController(navController = navController, Modifier.padding(padding))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.geohunt.app.authentication

import androidx.activity.ComponentActivity
import com.github.geohunt.app.model.database.api.User
import com.github.geohunt.app.utility.Singleton
import java.util.concurrent.CompletableFuture

interface Authenticator {

val user: User?

/**
* Begins an authentication phase from the given activity.
* Returns a future that will be completed when the user will be logged in.
* If the authentication fails, the completable future will fail.
* @param activity The activity that requests the connection
* @return A completable future that follows the above description
*/
fun authenticate(activity: ComponentActivity): CompletableFuture<User>

/**
* Signs out the user.
* @param activity The activity that requests the signing out
* @return The completable future is finished when the signing out is finished.
*/
fun signOut(activity: ComponentActivity): CompletableFuture<Void>

companion object {
val authInstance: Singleton<Authenticator> = Singleton(FirebaseAuthenticator())
}

}
Loading

0 comments on commit 8d1b6a0

Please sign in to comment.