Add quick fix in notification troubleshot test to perform a Firebase token rotation

This commit is contained in:
Benoit Marty 2024-10-30 09:39:44 +01:00 committed by Benoit Marty
parent 1f60509d92
commit a6fdb90838
12 changed files with 243 additions and 33 deletions

View file

@ -25,6 +25,7 @@ class FirebasePushProvider @Inject constructor(
private val firebaseStore: FirebaseStore,
private val pusherSubscriber: PusherSubscriber,
private val isPlayServiceAvailable: IsPlayServiceAvailable,
private val firebaseTokenRotator: FirebaseTokenRotator,
) : PushProvider {
override val index = FirebaseConfig.INDEX
override val name = FirebaseConfig.NAME
@ -71,6 +72,12 @@ class FirebasePushProvider @Inject constructor(
}
}
override fun canRotateToken(): Boolean = true
override suspend fun rotateToken(): Result<Unit> {
return firebaseTokenRotator.rotate()
}
companion object {
private val firebaseDistributor = Distributor("Firebase", "Firebase")
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.pushproviders.firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface FirebaseTokenDeleter {
suspend fun delete()
}
/**
* This class deletes the current Firebase token.
*/
@ContributesBinding(AppScope::class)
class DefaultFirebaseTokenDeleter @Inject constructor(
private val isPlayServiceAvailable: IsPlayServiceAvailable,
) : FirebaseTokenDeleter {
override suspend fun delete() {
suspendCoroutine { continuation ->
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (isPlayServiceAvailable.isAvailable()) {
try {
FirebaseMessaging.getInstance().deleteToken()
.addOnSuccessListener {
continuation.resume(Unit)
}
.addOnFailureListener { e ->
Timber.e(e, "## deleteFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} catch (e: Throwable) {
Timber.e(e, "## deleteFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} else {
val e = Exception("No valid Google Play Services found. Cannot use FCM.")
Timber.e(e)
throw e
}
}
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.pushproviders.firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface FirebaseTokenGetter {
suspend fun get(): String
}
/**
* This class read the current Firebase token.
* If the token does not exist, it will be generated.
*/
@ContributesBinding(AppScope::class)
class DefaultFirebaseTokenGetter @Inject constructor(
private val isPlayServiceAvailable: IsPlayServiceAvailable,
) : FirebaseTokenGetter {
override suspend fun get(): String {
return suspendCoroutine { continuation ->
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (isPlayServiceAvailable.isAvailable()) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
continuation.resume(token)
}
.addOnFailureListener { e ->
Timber.e(e, "## retrievedFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} catch (e: Throwable) {
Timber.e(e, "## retrievedFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} else {
val e = Exception("No valid Google Play Services found. Cannot use FCM.")
Timber.e(e)
continuation.resumeWithException(e)
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.pushproviders.firebase
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
interface FirebaseTokenRotator {
suspend fun rotate(): Result<Unit>
}
/**
* This class delete the Firebase token and generate a new one.
*/
@ContributesBinding(AppScope::class)
class DefaultFirebaseTokenRotator @Inject constructor(
private val firebaseTokenDeleter: FirebaseTokenDeleter,
private val firebaseTokenGetter: FirebaseTokenGetter,
) : FirebaseTokenRotator {
override suspend fun rotate(): Result<Unit> {
return runCatching {
firebaseTokenDeleter.delete()
firebaseTokenGetter.get()
}
}
}

View file

@ -7,14 +7,9 @@
package io.element.android.libraries.pushproviders.firebase
import com.google.firebase.messaging.FirebaseMessaging
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
interface FirebaseTroubleshooter {
suspend fun troubleshoot(): Result<Unit>
@ -26,37 +21,12 @@ interface FirebaseTroubleshooter {
@ContributesBinding(AppScope::class)
class DefaultFirebaseTroubleshooter @Inject constructor(
private val newTokenHandler: FirebaseNewTokenHandler,
private val isPlayServiceAvailable: IsPlayServiceAvailable,
private val firebaseTokenGetter: FirebaseTokenGetter,
) : FirebaseTroubleshooter {
override suspend fun troubleshoot(): Result<Unit> {
return runCatching {
val token = retrievedFirebaseToken()
val token = firebaseTokenGetter.get()
newTokenHandler.handle(token)
}
}
private suspend fun retrievedFirebaseToken(): String {
return suspendCoroutine { continuation ->
// 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features'
if (isPlayServiceAvailable.isAvailable()) {
try {
FirebaseMessaging.getInstance().token
.addOnSuccessListener { token ->
continuation.resume(token)
}
.addOnFailureListener { e ->
Timber.e(e, "## retrievedFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} catch (e: Throwable) {
Timber.e(e, "## retrievedFirebaseToken() : failed")
continuation.resumeWithException(e)
}
} else {
val e = Exception("No valid Google Play Services found. Cannot use FCM.")
Timber.e(e)
continuation.resumeWithException(e)
}
}
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.pushproviders.firebase
import io.element.android.tests.testutils.lambda.lambdaError
class FakeFirebaseTokenRotator(
private val rotateWithResult: () -> Result<Unit> = { lambdaError() }
) : FirebaseTokenRotator {
override suspend fun rotate(): Result<Unit> {
return rotateWithResult()
}
}

View file

@ -166,15 +166,27 @@ class FirebasePushProviderTest {
assertThat(result).isEqualTo(CurrentUserPushConfig(FirebaseConfig.PUSHER_HTTP_URL, "aToken"))
}
@Test
fun `rotateToken invokes the FirebaseTokenRotator`() = runTest {
val lambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val firebasePushProvider = createFirebasePushProvider(
firebaseTokenRotator = FakeFirebaseTokenRotator(lambda),
)
firebasePushProvider.rotateToken()
lambda.assertions().isCalledOnce()
}
private fun createFirebasePushProvider(
firebaseStore: FirebaseStore = InMemoryFirebaseStore(),
pusherSubscriber: PusherSubscriber = FakePusherSubscriber(),
isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
firebaseTokenRotator: FirebaseTokenRotator = FakeFirebaseTokenRotator(),
): FirebasePushProvider {
return FirebasePushProvider(
firebaseStore = firebaseStore,
pusherSubscriber = pusherSubscriber,
isPlayServiceAvailable = isPlayServiceAvailable,
firebaseTokenRotator = firebaseTokenRotator,
)
}
}