Skip to content

Commit

Permalink
test: More FireBase auth integration tests (#129)
Browse files Browse the repository at this point in the history
Co-authored-by: Joseph Cooper <[email protected]>
  • Loading branch information
grodin and Joseph Cooper authored Jul 24, 2023
1 parent 59661b0 commit 0e4b807
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.omricat.maplibrarian.auth.AuthError
import com.omricat.maplibrarian.auth.AuthService
import com.omricat.maplibrarian.auth.Credential
import com.omricat.maplibrarian.auth.EmailPasswordCredential
import com.omricat.maplibrarian.model.EmailAddress
import com.omricat.maplibrarian.model.User
import com.omricat.maplibrarian.model.UserUid
import com.omricat.maplibrarian.utils.DispatcherProvider
Expand Down Expand Up @@ -82,4 +83,15 @@ internal value class FirebaseUser(private val user: com.google.firebase.auth.Fir

override val id: UserUid
get() = UserUid(user.uid)
override val emailAddress: EmailAddress
get() {
val email =
user.email
?: error(
"""FirebaseUser email should always be non-null unless Multiple
| accounts per email has been enable in Firebase Console"""
.trimMargin()
)
return EmailAddress(email)
}
}
3 changes: 3 additions & 0 deletions core/src/main/kotlin/com/omricat/maplibrarian/model/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import kotlinx.serialization.Serializable

@Serializable @JvmInline public value class UserUid(public val value: String)

@Serializable @JvmInline public value class EmailAddress(public val value: String)

public interface User {
public val displayName: String
public val id: UserUid
public val emailAddress: EmailAddress

public companion object
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class ActualAuthWorkflowTest :
"onAuthenticated action outputs Authenticated(user) from workflow" {
val workflow = ActualAuthWorkflow(TestAuthService(), NullSignupWorkflow)
val fakeCredential = EmailPasswordCredential("[email protected]", "12345")
val fakeUser = TestUser("user1", UserUid("1"))
val fakeUser = TestUser("user1", UserUid("1"), "[email protected]")
val (_, maybeOutput) =
workflow
.onAuthenticated(fakeUser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ internal class ActualSignUpWorkflowTest :
"onUserCreated sets output to created user" {
val workflow = ActualSignUpWorkflow(TestAuthService())
val credential = EmailPasswordCredential("blah@blah", "password")
val user = TestUser("user1", UserUid("1234"))
val user = TestUser("user1", UserUid("1234"), "[email protected]")

val (_, maybeOutput) =
workflow
Expand Down
12 changes: 11 additions & 1 deletion core/src/test/kotlin/com/omricat/maplibrarian/auth/TestUser.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package com.omricat.maplibrarian.auth

import com.omricat.maplibrarian.model.EmailAddress
import com.omricat.maplibrarian.model.User
import com.omricat.maplibrarian.model.UserUid

internal data class TestUser(override val displayName: String, override val id: UserUid) : User
internal data class TestUser(
override val displayName: String,
override val id: UserUid,
override val emailAddress: EmailAddress
) : User {
companion object {
operator fun invoke(displayName: String, id: UserUid, emailAddress: String): TestUser =
TestUser(displayName, id, EmailAddress(emailAddress))
}
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[versions]
assertk = "0.26.1"
beagle = "2.9.0"
detekt = "1.18.1"
firebase = "29.0.0"
Expand Down Expand Up @@ -57,13 +58,16 @@ firebase_firestoreKtx = { group = "com.google.firebase", name = "firebase-firest
firebase_authKtx = { group = "com.google.firebase", name = "firebase-auth-ktx" }

retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit2" }
retrofit_converter_kotlinXSerialization = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version = "1.0.0" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp3" }

material = { group = "com.google.android.material", name = "material", version.ref = "material" }

jakeWharton_processPhoenix = { module = "com.jakewharton:process-phoenix", version.ref = "processPhoenix" }
jakeWharton_timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }

assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" }

# Used in build-logic
android_gradlePlugin_api = { module = "com.android.tools.build:gradle-api", version.ref = "agp" }
android_gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
Expand Down
15 changes: 13 additions & 2 deletions integration-tests/firebase/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("UnstableApiUsage")

import com.android.build.api.dsl.ManagedVirtualDevice
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
Expand All @@ -12,7 +14,10 @@ buildscript {
dependencies { classpath(libs.okhttp) }
}

plugins { alias(libs.plugins.maplib.android.test) }
plugins {
alias(libs.plugins.maplib.android.test)
alias(libs.plugins.kotlin.serialization)
}

android {
namespace = "com.omricat.maplibrarian.integrationtesting.debug"
Expand Down Expand Up @@ -80,18 +85,24 @@ dependencies {
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.test)

implementation(platform(libs.kotlinx.serialization.bom))
implementation(libs.kotlinx.serialization.json)

implementation(platform(libs.firebase.bom))
implementation(libs.firebase.firestoreKtx)
implementation(libs.firebase.authKtx)

implementation(libs.retrofit)
implementation(libs.retrofit.converter.kotlinXSerialization)

implementation(projects.util.kotlinResultAssertkExtensions)
implementation(libs.kotlinResult)
implementation(libs.kotlinResult.coroutines)

implementation(androidx.test.coreKtx)

implementation(androidx.test.runner)
implementation(androidx.test.ext.junitKtx)
implementation(androidx.test.ext.truth)

implementation(libs.assertk)
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,75 @@
package com.omricat.maplibrarian.firebase.auth

import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import com.omricat.maplibrarian.auth.EmailPasswordCredential
import com.omricat.maplibrarian.model.EmailAddress
import com.omricat.maplibrarian.model.User
import com.omricat.maplibrarian.model.UserUid
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.create
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path

class FirebaseAuthEmulatorRestApi(private val projectId: String, baseUrl: HttpUrl) {

private val jsonMediaType = "application/json; charset=utf-8".toMediaType()

private interface AuthEmulatorApi {

@DELETE("/emulator/v1/projects/{project-id}/accounts")
fun deleteAllUsers(@Path("project-id") projectId: String): Call<Unit>

@Headers("Authorization: Bearer owner")
@POST("/identitytoolkit.googleapis.com/v1/projects/{project-id}/accounts")
fun createUser(
@Path("project-id") projectId: String,
@Body body: RequestBody
): Call<TestUser>
}

@Serializable
data class TestUser(override val displayName: String, val localId: String, val email: String) :
User {
override val id: UserUid = UserUid(localId)
override val emailAddress: EmailAddress = EmailAddress(email)
}

private val wrappedApi: AuthEmulatorApi = Retrofit.Builder().baseUrl(baseUrl).build().create()
private val json = Json { ignoreUnknownKeys = true }

private val wrappedApi: AuthEmulatorApi =
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(json.asConverterFactory(jsonMediaType))
.build()
.create()
fun deleteAllUsers(): Response<Unit> = wrappedApi.deleteAllUsers(projectId).execute()

fun createUser(credential: EmailPasswordCredential): Response<TestUser> {

val jsonBody =
"""{
"customAttributes":"",
"displayName":"",
"photoUrl":"",
"email":"${credential.emailAddress}",
"password":"${credential.password}",
"phoneNumber":"",
"emailVerified":false,
"mfaInfo":[]
}"""
.toRequestBody(contentType = jsonMediaType)
val call = wrappedApi.createUser(projectId, jsonBody)
return call.execute()
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.omricat.maplibrarian.firebase.auth

import com.github.michaelbull.result.Ok
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.prop
import com.google.firebase.auth.FirebaseAuth
import com.omricat.maplibrarian.auth.EmailPasswordCredential
import com.omricat.maplibrarian.firebase.FirebaseEmulatorConnection
import com.omricat.maplibrarian.firebase.TestDispatcherProvider
import com.omricat.maplibrarian.firebase.TestFixtures
import com.omricat.maplibrarian.model.User
import com.omricat.maplibrarian.firebase.auth.FirebaseAuthEmulatorRestApi.TestUser
import com.omricat.result.kotest.assertk.isOk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
Expand All @@ -28,7 +31,29 @@ class FirebaseAuthServiceTest {
val repository =
FirebaseAuthService(firebaseAuthInstance, TestDispatcherProvider(testScheduler))
val createUserResult = repository.createUser(testCredential)
assert(createUserResult is Ok<User>)
assertThat(createUserResult).isOk()
}

@Test
fun canSignInAddedUser() = runTest {
val repository =
FirebaseAuthService(firebaseAuthInstance, TestDispatcherProvider(testScheduler))
val createUserResult = repository.createUser(testCredential)
repository.signOut()
val signInResult = repository.attemptAuthentication(testCredential)
assertThat(signInResult).isOk()
}

@Test
fun canSignInExternallyAddedUser() = runTest {
val createdUser = Fixtures.createUserViaRestApi(testCredential)
val repository =
FirebaseAuthService(firebaseAuthInstance, TestDispatcherProvider(testScheduler))
val signInResult = repository.attemptAuthentication(testCredential)
assertThat(signInResult)
.isOk()
.prop("Email address") { it.emailAddress.value }
.isEqualTo(createdUser.email)
}

companion object Fixtures {
Expand All @@ -53,5 +78,9 @@ class FirebaseAuthServiceTest {
TestFixtures.emulatorBaseUrl(FirebaseEmulatorConnection.AUTH_PORT)
)
}

fun createUserViaRestApi(credential: EmailPasswordCredential): TestUser =
authApi.createUser(credential).body()
?: error("Failed to create user with credentials $credential")
}
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ rootProject.name = ("map-librarian")
include(
":app",
":core",
":kotlin-result-kotest",
":util:kotlin-result-assertk-extensions",
":integration-tests:firebase",
)
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins { alias(libs.plugins.maplib.kotlin.library) }
dependencies {
api(libs.kotlinResult)
api(libs.kotest.assertions.core)
api(libs.assertk)
}

kotlin { sourceSets.all { languageSettings.optIn("kotlin.contracts.ExperimentalContracts") } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.omricat.result.kotest.assertk

import assertk.Assert
import assertk.assertions.isInstanceOf
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result

public fun <V> Assert<Result<V, *>>.isOk(): Assert<V> = isInstanceOf<Ok<V>>().transform { it.value }

public fun <E> Assert<Result<*, E>>.isErr(): Assert<E> =
isInstanceOf<Err<E>>().transform { it.error }

0 comments on commit 0e4b807

Please sign in to comment.