feat(sdk): AuthGoogleApp mutation and Google ID token auth without Firebase
Made-with: Cursor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
mutation AuthGoogleNew($accessToken: String!) {
|
||||
mutation AuthGoogleApp($accessToken: String!) {
|
||||
authGoogleApp(input: { accessToken: $accessToken }) {
|
||||
token
|
||||
name
|
||||
@@ -4,7 +4,7 @@ import com.apollographql.apollo.ApolloClient
|
||||
import com.apollographql.apollo.api.Optional
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import org.db3.airmq.sdk.AuthGoogleNewMutation
|
||||
import org.db3.airmq.sdk.AuthGoogleAppMutation
|
||||
import org.db3.airmq.sdk.LoginLocalMutation
|
||||
import org.db3.airmq.sdk.RegisterMutation
|
||||
import org.db3.airmq.sdk.auth.model.AuthProvider
|
||||
@@ -82,28 +82,22 @@ class FirebaseAuthService @Inject constructor(
|
||||
localEmailAuthStore.clearProfile().getOrThrow()
|
||||
}
|
||||
|
||||
private suspend fun signInWithGoogle(token: String): User {
|
||||
/**
|
||||
* Google OAuth: send the Google ID token from Credential Manager to [authGoogleApp], then persist the API JWT
|
||||
* (same storage as email login). Does not sign in to Firebase; the backend verifies the Google JWT.
|
||||
*/
|
||||
private suspend fun signInWithGoogle(googleIdToken: String): User {
|
||||
localEmailAuthStore.clearProfile().getOrThrow()
|
||||
val firebaseSessionUser = firebaseSessionManager.signInWithGoogle(token)
|
||||
try {
|
||||
val apiToken = exchangeFirebaseIdTokenWithBackend(firebaseSessionUser.firebaseAccessToken)
|
||||
apiTokenStore.saveToken(apiToken).getOrThrow()
|
||||
return firebaseSessionUser.user
|
||||
} catch (e: Throwable) {
|
||||
rollbackSessionAfterFailedGoogleBackendAuth()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** Firebase may already be signed in when the backend exchange fails; clear everything so the user stays logged out. */
|
||||
private suspend fun rollbackSessionAfterFailedGoogleBackendAuth() {
|
||||
try {
|
||||
firebaseSessionManager.signOut()
|
||||
apiTokenStore.clearToken().getOrThrow()
|
||||
localEmailAuthStore.clearProfile().getOrThrow()
|
||||
} catch (_: Throwable) {
|
||||
// Best-effort; caller still receives the original sign-in failure.
|
||||
val response = apolloClient
|
||||
.mutation(AuthGoogleAppMutation(accessToken = googleIdToken))
|
||||
.execute()
|
||||
response.exception?.let { throw it }
|
||||
response.errors?.firstOrNull()?.let { gqlError ->
|
||||
throw IllegalStateException(gqlError.message)
|
||||
}
|
||||
val auth = response.data?.authGoogleApp
|
||||
?: error("Google sign-in response missing data.")
|
||||
return commitEmailAuthSession(auth.token, auth._id, auth.email, auth.name)
|
||||
}
|
||||
|
||||
private suspend fun commitEmailAuthSession(
|
||||
@@ -130,16 +124,4 @@ class FirebaseAuthService @Inject constructor(
|
||||
isAuthenticated = true
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun exchangeFirebaseIdTokenWithBackend(firebaseIdToken: String): String {
|
||||
val response = apolloClient
|
||||
.mutation(AuthGoogleNewMutation(accessToken = firebaseIdToken))
|
||||
.execute()
|
||||
response.exception?.let { throw it }
|
||||
response.errors?.firstOrNull()?.let { gqlError ->
|
||||
throw IllegalStateException(gqlError.message)
|
||||
}
|
||||
return response.data?.authGoogleApp?.token
|
||||
?: error("Backend auth exchange succeeded without API token.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,5 @@ import org.db3.airmq.sdk.auth.model.User
|
||||
interface FirebaseSessionManager {
|
||||
suspend fun getUser(): User?
|
||||
suspend fun isSignedIn(): Boolean
|
||||
suspend fun signInWithGoogle(idToken: String): FirebaseSessionUser
|
||||
suspend fun signOut()
|
||||
}
|
||||
|
||||
data class FirebaseSessionUser(
|
||||
val user: User,
|
||||
val firebaseAccessToken: String
|
||||
)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
package org.db3.airmq.sdk.auth
|
||||
|
||||
import com.google.android.gms.tasks.Task
|
||||
import com.google.firebase.auth.FirebaseAuth
|
||||
import com.google.firebase.auth.FirebaseUser
|
||||
import com.google.firebase.auth.GoogleAuthProvider
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.db3.airmq.sdk.auth.model.User
|
||||
|
||||
@Singleton
|
||||
@@ -20,18 +15,6 @@ class FirebaseSessionManagerImpl @Inject constructor(
|
||||
|
||||
override suspend fun isSignedIn(): Boolean = firebaseAuth.currentUser != null
|
||||
|
||||
override suspend fun signInWithGoogle(idToken: String): FirebaseSessionUser {
|
||||
val credential = GoogleAuthProvider.getCredential(idToken, null)
|
||||
val authResult = firebaseAuth.signInWithCredential(credential).awaitResult()
|
||||
val firebaseUser = authResult.user ?: error("Google sign-in completed without a Firebase user.")
|
||||
val firebaseAccessToken = firebaseUser.getIdToken(true).awaitResult().token
|
||||
?: error("Firebase token exchange completed without an ID token.")
|
||||
return FirebaseSessionUser(
|
||||
user = firebaseUser.toUser(),
|
||||
firebaseAccessToken = firebaseAccessToken
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun signOut() {
|
||||
firebaseAuth.signOut()
|
||||
}
|
||||
@@ -43,15 +26,3 @@ class FirebaseSessionManagerImpl @Inject constructor(
|
||||
isAuthenticated = true
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> Task<T>.awaitResult(): T = suspendCancellableCoroutine { continuation ->
|
||||
addOnSuccessListener { result ->
|
||||
continuation.resume(result)
|
||||
}
|
||||
addOnFailureListener { exception ->
|
||||
continuation.resumeWithException(exception)
|
||||
}
|
||||
addOnCanceledListener {
|
||||
continuation.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.db3.airmq.sdk.network
|
||||
import android.util.Log
|
||||
import com.apollographql.apollo.api.ApolloRequest
|
||||
import com.apollographql.apollo.api.ApolloResponse
|
||||
import com.apollographql.apollo.api.Error
|
||||
import com.apollographql.apollo.api.Operation
|
||||
import com.apollographql.apollo.interceptor.ApolloInterceptor
|
||||
import com.apollographql.apollo.interceptor.ApolloInterceptorChain
|
||||
@@ -12,6 +13,9 @@ import kotlinx.coroutines.flow.onEach
|
||||
/**
|
||||
* Logs every GraphQL operation name, document, variable payload ([Operation] `toString`),
|
||||
* and response data or errors to logcat (tag [TAG]).
|
||||
*
|
||||
* GraphQL [Error]s are logged with [Error.path], [Error.extensions] (e.g. `code`, `stacktrace`),
|
||||
* and [Error.nonStandardFields], not only message and locations.
|
||||
*/
|
||||
class ApolloLoggingInterceptor : ApolloInterceptor {
|
||||
override fun <D : Operation.Data> intercept(
|
||||
@@ -27,36 +31,145 @@ class ApolloLoggingInterceptor : ApolloInterceptor {
|
||||
if (response.exception != null) {
|
||||
Log.e(
|
||||
TAG,
|
||||
"---------- GraphQL response: $operationName FAILED ----------",
|
||||
"---------- GraphQL response: $operationName FAILED (transport / parse) ----------",
|
||||
response.exception
|
||||
)
|
||||
logGraphqlErrorsIfAny(operationName, response)
|
||||
logResponseExtensionsIfAny(operationName, response)
|
||||
val dataStr = response.data?.toString() ?: "null"
|
||||
logChunked("$TAG.response.$operationName.data", dataStr)
|
||||
} else {
|
||||
Log.d(TAG, "---------- GraphQL response: $operationName ----------")
|
||||
val errors = response.errors
|
||||
if (!errors.isNullOrEmpty()) {
|
||||
errors.forEachIndexed { i, err ->
|
||||
Log.w(TAG, "errors[$i]: ${err.message} (locations=${err.locations})")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "errors: none")
|
||||
}
|
||||
logGraphqlErrorsIfAny(operationName, response)
|
||||
logResponseExtensionsIfAny(operationName, response)
|
||||
val dataStr = response.data?.toString() ?: "null"
|
||||
logChunked("$TAG.response.$operationName.data", dataStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Android single-line log limit is ~4k; split large payloads. */
|
||||
private fun logChunked(label: String, text: String, chunkSize: Int = 3500) {
|
||||
private fun <D : Operation.Data> logResponseExtensionsIfAny(
|
||||
operationName: String,
|
||||
response: ApolloResponse<D>
|
||||
) {
|
||||
val ext = response.extensions
|
||||
if (ext.isEmpty()) {
|
||||
Log.d(TAG, "response.extensions: none")
|
||||
return
|
||||
}
|
||||
logChunked(
|
||||
"$TAG.response.$operationName.protocolExtensions",
|
||||
ext.entries.joinToString(prefix = "{", postfix = "}") { (k, v) ->
|
||||
"\n $k = ${formatExtensionValue(v)}"
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun <D : Operation.Data> logGraphqlErrorsIfAny(
|
||||
operationName: String,
|
||||
response: ApolloResponse<D>
|
||||
) {
|
||||
val errors = response.errors
|
||||
if (errors.isNullOrEmpty()) {
|
||||
Log.d(TAG, "graphql.errors: none")
|
||||
return
|
||||
}
|
||||
Log.w(TAG, "graphql.errors: count=${errors.size} (partial data may still be present)")
|
||||
errors.forEachIndexed { index, err ->
|
||||
val detail = formatGraphqlErrorDetail(err)
|
||||
logChunked(
|
||||
"$TAG.response.$operationName.graphqlError[$index]",
|
||||
detail,
|
||||
priority = Log.WARN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatGraphqlErrorDetail(error: Error): String = buildString {
|
||||
appendLine("message: ${error.message}")
|
||||
val path = error.path
|
||||
if (!path.isNullOrEmpty()) {
|
||||
appendLine("path: ${path.joinToString(prefix = "[", postfix = "]")}")
|
||||
} else {
|
||||
appendLine("path: (none)")
|
||||
}
|
||||
val locs = error.locations
|
||||
if (!locs.isNullOrEmpty()) {
|
||||
appendLine(
|
||||
"locations: " + locs.joinToString { loc ->
|
||||
"line=${loc.line}, column=${loc.column}"
|
||||
}
|
||||
)
|
||||
} else {
|
||||
appendLine("locations: (none)")
|
||||
}
|
||||
appendLine("extensions:")
|
||||
appendIndentedMapOrEmpty(error.extensions, indent = 2)
|
||||
appendLine("nonStandardFields:")
|
||||
appendIndentedMapOrEmpty(error.nonStandardFields, indent = 2)
|
||||
}
|
||||
|
||||
private fun StringBuilder.appendIndentedMapOrEmpty(
|
||||
map: Map<String, Any?>?,
|
||||
indent: Int
|
||||
) {
|
||||
val prefix = " ".repeat(indent)
|
||||
if (map.isNullOrEmpty()) {
|
||||
appendLine("$prefix(none)")
|
||||
return
|
||||
}
|
||||
map.forEach { (k, v) ->
|
||||
append(prefix)
|
||||
append(k)
|
||||
append(" = ")
|
||||
appendLine(formatExtensionValue(v, indent + 2))
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatExtensionValue(value: Any?, nestedIndent: Int = 0): String = when (value) {
|
||||
null -> "null"
|
||||
is Map<*, *> -> {
|
||||
val prefix = "\n" + " ".repeat(nestedIndent)
|
||||
value.entries.joinToString(prefix = "{", postfix = "}") { (k, v) ->
|
||||
"$prefix${k.toString()} = ${formatExtensionValue(v, nestedIndent + 2)}"
|
||||
}
|
||||
}
|
||||
is Iterable<*> -> {
|
||||
val prefix = "\n" + " ".repeat(nestedIndent)
|
||||
value.withIndex().joinToString(prefix = "[", postfix = "]") { (i, v) ->
|
||||
"$prefix[$i] ${formatExtensionValue(v, nestedIndent + 2)}"
|
||||
}
|
||||
}
|
||||
is Array<*> -> formatExtensionValue(value.toList(), nestedIndent)
|
||||
else -> value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Android single-line log limit is ~4k; split large payloads.
|
||||
* @param priority [Log.DEBUG] for normal traffic; [Log.WARN] for GraphQL error bodies.
|
||||
*/
|
||||
private fun logChunked(
|
||||
label: String,
|
||||
text: String,
|
||||
chunkSize: Int = 3500,
|
||||
priority: Int = Log.DEBUG
|
||||
) {
|
||||
val logLine: (String) -> Unit = { line ->
|
||||
when (priority) {
|
||||
Log.WARN -> Log.w(TAG, line)
|
||||
Log.ERROR -> Log.e(TAG, line)
|
||||
else -> Log.d(TAG, line)
|
||||
}
|
||||
}
|
||||
if (text.length <= chunkSize) {
|
||||
Log.d(TAG, "$label: $text")
|
||||
logLine("$label: $text")
|
||||
return
|
||||
}
|
||||
var start = 0
|
||||
var part = 0
|
||||
while (start < text.length) {
|
||||
val end = minOf(start + chunkSize, text.length)
|
||||
Log.d(TAG, "$label [part $part]: ${text.substring(start, end)}")
|
||||
logLine("$label [part $part]: ${text.substring(start, end)}")
|
||||
part++
|
||||
start = end
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package org.db3.airmq.sdk.auth
|
||||
|
||||
import com.apollographql.apollo.ApolloClient
|
||||
import com.apollographql.apollo.api.ApolloResponse
|
||||
import com.benasher44.uuid.uuid4
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.db3.airmq.sdk.AuthGoogleNewMutation
|
||||
import org.db3.airmq.sdk.AuthGoogleAppMutation
|
||||
import org.db3.airmq.sdk.auth.model.AuthProvider
|
||||
import org.db3.airmq.sdk.auth.model.User
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -20,32 +22,12 @@ import org.junit.Test
|
||||
class FirebaseAuthServiceTest {
|
||||
|
||||
@Test
|
||||
fun signIn_google_failsWhenFirebaseSignInFails() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(signInError = IllegalStateException("firebase failed"))
|
||||
fun signIn_google_failsWhenAuthGoogleAppFails() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(user = null, isSignedIn = false)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore()
|
||||
val localEmailStore = FakeLocalEmailAuthStore()
|
||||
val service = FirebaseAuthService(sessionManager, apolloClient, tokenStore, localEmailStore)
|
||||
|
||||
val result = service.signIn(provider = AuthProvider.GOOGLE, token = "google-id-token")
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertEquals("firebase failed", result.exceptionOrNull()?.message)
|
||||
assertEquals(null, tokenStore.storedToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signIn_google_failsWhenBackendExchangeFails() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(
|
||||
signInResult = FirebaseSessionUser(
|
||||
user = User("uid-1", "user@airmq.cc", "User", true),
|
||||
firebaseAccessToken = "firebase-token"
|
||||
)
|
||||
)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore()
|
||||
val localEmailStore = FakeLocalEmailAuthStore()
|
||||
mockAuthGoogleNewError(apolloClient, IllegalStateException("backend failed"))
|
||||
mockAuthGoogleAppError(apolloClient, IllegalStateException("backend failed"))
|
||||
val service = FirebaseAuthService(sessionManager, apolloClient, tokenStore, localEmailStore)
|
||||
|
||||
val result = service.signIn(provider = AuthProvider.GOOGLE, token = "google-id-token")
|
||||
@@ -53,17 +35,48 @@ class FirebaseAuthServiceTest {
|
||||
assertTrue(result.isFailure)
|
||||
assertEquals("backend failed", result.exceptionOrNull()?.message)
|
||||
assertEquals(null, tokenStore.storedToken)
|
||||
assertEquals(0, sessionManager.signOutCallCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signIn_google_succeedsWhenAuthGoogleAppReturnsToken() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(user = null, isSignedIn = false)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore()
|
||||
val localEmailStore = FakeLocalEmailAuthStore()
|
||||
val mockCall = mockk<com.apollographql.apollo.ApolloCall<AuthGoogleAppMutation.Data>>()
|
||||
every { apolloClient.mutation(any<AuthGoogleAppMutation>()) } returns mockCall
|
||||
val mutationData = AuthGoogleAppMutation.Data(
|
||||
authGoogleApp = AuthGoogleAppMutation.AuthGoogleApp(
|
||||
token = "api-jwt",
|
||||
name = "N",
|
||||
email = "u@test.dev",
|
||||
roles = null,
|
||||
_id = "backend-id",
|
||||
regDate = null
|
||||
)
|
||||
)
|
||||
val apolloResponse = ApolloResponse.Builder(
|
||||
AuthGoogleAppMutation(accessToken = "google-id-token"),
|
||||
uuid4()
|
||||
).data(mutationData).build()
|
||||
coEvery { mockCall.execute() } returns apolloResponse
|
||||
val service = FirebaseAuthService(sessionManager, apolloClient, tokenStore, localEmailStore)
|
||||
|
||||
val result = service.signIn(provider = AuthProvider.GOOGLE, token = "google-id-token")
|
||||
|
||||
assertTrue(result.isSuccess)
|
||||
assertEquals("api-jwt", tokenStore.storedToken)
|
||||
assertEquals("backend-id", localEmailStore.getProfile()?.userId)
|
||||
assertEquals("u@test.dev", localEmailStore.getProfile()?.email)
|
||||
assertEquals(1, sessionManager.signOutCallCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isAuthenticated_trueWhenFirebaseUserAndApiTokenPresent() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(
|
||||
isSignedIn = true,
|
||||
signInResult = FirebaseSessionUser(
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true),
|
||||
firebaseAccessToken = "ft"
|
||||
)
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true),
|
||||
isSignedIn = true
|
||||
)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
|
||||
@@ -78,7 +91,7 @@ class FirebaseAuthServiceTest {
|
||||
|
||||
@Test
|
||||
fun isAuthenticated_trueWhenLocalEmailProfileAndApiTokenPresent() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(isSignedIn = false, signInResult = null)
|
||||
val sessionManager = FakeFirebaseSessionManager(user = null, isSignedIn = false)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
|
||||
val localEmailStore = FakeLocalEmailAuthStore().apply {
|
||||
@@ -92,10 +105,7 @@ class FirebaseAuthServiceTest {
|
||||
@Test
|
||||
fun getUser_returnsNullWhenFirebaseUserButNoApiToken() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(
|
||||
signInResult = FirebaseSessionUser(
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true),
|
||||
firebaseAccessToken = "ft"
|
||||
)
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true)
|
||||
)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = null)
|
||||
@@ -108,10 +118,7 @@ class FirebaseAuthServiceTest {
|
||||
@Test
|
||||
fun getUser_returnsFirebaseUserWhenApiTokenPresentAndNoLocalProfile() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(
|
||||
signInResult = FirebaseSessionUser(
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true),
|
||||
firebaseAccessToken = "ft"
|
||||
)
|
||||
user = User("uid-fb", "fb@test.dev", "FB", true)
|
||||
)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
|
||||
@@ -126,7 +133,7 @@ class FirebaseAuthServiceTest {
|
||||
|
||||
@Test
|
||||
fun getUser_returnsLocalProfileWhenNoFirebaseUser() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(isSignedIn = false, signInResult = null)
|
||||
val sessionManager = FakeFirebaseSessionManager(user = null, isSignedIn = false)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
|
||||
val localEmailStore = FakeLocalEmailAuthStore().apply {
|
||||
@@ -144,7 +151,7 @@ class FirebaseAuthServiceTest {
|
||||
|
||||
@Test
|
||||
fun signOut_clearsTokenAndLocalProfile() = runTest {
|
||||
val sessionManager = FakeFirebaseSessionManager(isSignedIn = false, signInResult = null)
|
||||
val sessionManager = FakeFirebaseSessionManager(user = null, isSignedIn = false)
|
||||
val apolloClient = mockk<ApolloClient>()
|
||||
val tokenStore = FakeApiTokenStore(storedToken = "api-token")
|
||||
val localEmailStore = FakeLocalEmailAuthStore().apply {
|
||||
@@ -157,30 +164,24 @@ class FirebaseAuthServiceTest {
|
||||
assertNull(localEmailStore.getProfile())
|
||||
}
|
||||
|
||||
private fun mockAuthGoogleNewError(apolloClient: ApolloClient, error: Throwable) {
|
||||
val mockCall = mockk<com.apollographql.apollo.ApolloCall<AuthGoogleNewMutation.Data>>()
|
||||
every { apolloClient.mutation(any<AuthGoogleNewMutation>()) } returns mockCall
|
||||
private fun mockAuthGoogleAppError(apolloClient: ApolloClient, error: Throwable) {
|
||||
val mockCall = mockk<com.apollographql.apollo.ApolloCall<AuthGoogleAppMutation.Data>>()
|
||||
every { apolloClient.mutation(any<AuthGoogleAppMutation>()) } returns mockCall
|
||||
coEvery { mockCall.execute() } throws error
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeFirebaseSessionManager(
|
||||
private val signInResult: FirebaseSessionUser? = null,
|
||||
private val signInError: Throwable? = null,
|
||||
private val user: User? = null,
|
||||
private val isSignedIn: Boolean = true
|
||||
) : FirebaseSessionManager {
|
||||
var signOutCallCount = 0
|
||||
private set
|
||||
|
||||
override suspend fun getUser(): User? = signInResult?.user
|
||||
override suspend fun getUser(): User? = user
|
||||
|
||||
override suspend fun isSignedIn(): Boolean = isSignedIn
|
||||
|
||||
override suspend fun signInWithGoogle(idToken: String): FirebaseSessionUser {
|
||||
signInError?.let { throw it }
|
||||
return signInResult ?: error("No signInResult configured")
|
||||
}
|
||||
|
||||
override suspend fun signOut() {
|
||||
signOutCallCount++
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user