diff --git a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt index 074009cacc0..0a0b507742d 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/intent/IntentResolverTest.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser +import io.element.android.tests.testutils.lambda.lambdaError import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith @@ -229,7 +230,7 @@ class IntentResolverTest { } private fun createIntentResolver( - permalinkParserResult: () -> PermalinkData = { throw NotImplementedError() } + permalinkParserResult: () -> PermalinkData = { lambdaError() } ): IntentResolver { return IntentResolver( deeplinkParser = DeeplinkParser(), diff --git a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt index 247df956e64..72552b9ed1c 100644 --- a/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt +++ b/features/invite/impl/src/test/kotlin/io/element/android/features/invite/impl/response/AcceptDeclineInvitePresenterTest.kt @@ -192,9 +192,11 @@ class AcceptDeclineInvitePresenterTest { cancelAndConsumeRemainingEvents() } assert(joinRoomFailure) - .isCalledExactly(1) - .withSequence( - listOf(value(A_ROOM_ID), value(emptyList()), value(JoinedRoom.Trigger.Invite)) + .isCalledOnce() + .with( + value(A_ROOM_ID), + value(emptyList()), + value(JoinedRoom.Trigger.Invite) ) } @@ -221,9 +223,11 @@ class AcceptDeclineInvitePresenterTest { cancelAndConsumeRemainingEvents() } assert(joinRoomSuccess) - .isCalledExactly(1) - .withSequence( - listOf(value(A_ROOM_ID), value(emptyList()), value(JoinedRoom.Trigger.Invite)) + .isCalledOnce() + .with( + value(A_ROOM_ID), + value(emptyList()), + value(JoinedRoom.Trigger.Invite) ) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt index e058d4691bc..a6e9c63208a 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/notifications/NotificationSettingsPresenterTests.kt @@ -19,12 +19,12 @@ package io.element.android.features.preferences.impl.notifications import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test -import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.tests.testutils.consumeItemsUntilPredicate import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt index e0c033afc71..69617f30f82 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportPresenterTest.kt @@ -136,7 +136,7 @@ class BugReportPresenterTest { initialState.eventSink.invoke(BugReportEvents.ResetAll) val resetState = awaitItem() assertThat(resetState.hasCrashLogs).isFalse() - logFilesRemoverLambda.assertions().isCalledExactly(1) + logFilesRemoverLambda.assertions().isCalledOnce() // TODO Make it live assertThat(resetState.screenshotUri).isNull() } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt index 8bbdf47e21b..6296f219def 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/join/DefaultJoinRoomTest.kt @@ -57,9 +57,9 @@ class DefaultJoinRoomTest { .isNeverCalled() joinRoomLambda .assertions() - .isCalledExactly(1) - .withSequence( - listOf(value(A_ROOM_ID)) + .isCalledOnce() + .with( + value(A_ROOM_ID) ) assertThat(analyticsService.capturedEvents).containsExactly( roomResult.toAnalyticsJoinedRoom(aTrigger) @@ -88,9 +88,10 @@ class DefaultJoinRoomTest { sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger) joinRoomByIdOrAliasLambda .assertions() - .isCalledExactly(1) - .withSequence( - listOf(value(A_ROOM_ID), value(A_SERVER_LIST)) + .isCalledOnce() + .with( + value(A_ROOM_ID), + value(A_SERVER_LIST) ) joinRoomLambda .assertions() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index e507e340332..d10d1ad0d24 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings const val A_USER_NAME = "alice" const val A_PASSWORD = "password" +const val A_SECRET = "secret" val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index c24df7d717b..b138c55acbf 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -31,7 +31,9 @@ import kotlinx.coroutines.flow.flowOf val A_OIDC_DATA = OidcDetails(url = "a-url") -class FakeAuthenticationService : MatrixAuthenticationService { +class FakeAuthenticationService( + private val matrixClientResult: ((SessionId) -> Result)? = null +) : MatrixAuthenticationService { private val homeserver = MutableStateFlow(null) private var oidcError: Throwable? = null private var oidcCancelError: Throwable? = null @@ -48,6 +50,9 @@ class FakeAuthenticationService : MatrixAuthenticationService { override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda() override suspend fun restoreSession(sessionId: SessionId): Result { + if (matrixClientResult != null) { + return matrixClientResult.invoke(sessionId) + } return if (matrixClient != null) { Result.success(matrixClient!!) } else { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt index d1ffb70f99f..525746e690f 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/permalink/FakePermalinkParser.kt @@ -18,9 +18,10 @@ package io.element.android.libraries.matrix.test.permalink import io.element.android.libraries.matrix.api.permalink.PermalinkData import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.tests.testutils.lambda.lambdaError class FakePermalinkParser( - private var result: () -> PermalinkData = { TODO("Not implemented") } + private var result: () -> PermalinkData = { lambdaError() } ) : PermalinkParser { fun givenResult(result: PermalinkData) { this.result = { result } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt index 05a40e99952..3ede3b272f1 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt @@ -19,8 +19,12 @@ package io.element.android.libraries.matrix.test.pushers import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData +import io.element.android.tests.testutils.lambda.lambdaError -class FakePushersService : PushersService { - override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) - override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result = Result.success(Unit) +class FakePushersService( + private val setHttpPusherResult: (SetHttpPusherData) -> Result = { lambdaError() }, + private val unsetHttpPusherResult: (UnsetHttpPusherData) -> Result = { lambdaError() }, +) : PushersService { + override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = setHttpPusherResult(setHttpPusherData) + override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result = unsetHttpPusherResult(unsetHttpPusherData) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 607213953d5..ce27acb7b32 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -21,9 +21,6 @@ import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider interface PushService { - // TODO Move away - fun notificationStyleChanged() - /** * Return the current push provider, or null if none. */ diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index ee528a4ae7a..05e249798fa 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) + testImplementation(projects.libraries.pushstore.test) testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 4396337883f..bf2b1ed5422 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager +import io.element.android.libraries.push.impl.test.TestPush import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushstore.api.UserPushStoreFactory @@ -30,16 +30,11 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( - private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, - private val pushersManager: PushersManager, + private val testPush: TestPush, private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, private val getCurrentPushProvider: GetCurrentPushProvider, ) : PushService { - override fun notificationStyleChanged() { - defaultNotificationDrawerManager.notificationStyleChanged() - } - override suspend fun getCurrentPushProvider(): PushProvider? { val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider() return pushProviders.find { it.name == currentPushProvider } @@ -51,9 +46,6 @@ class DefaultPushService @Inject constructor( .sortedBy { it.index } } - /** - * Get current push provider, compare with provided one, then unregister and register if different, and store change. - */ override suspend fun registerWith( matrixClient: MatrixClient, pushProvider: PushProvider, @@ -80,7 +72,7 @@ class DefaultPushService @Inject constructor( override suspend fun testPush(): Boolean { val pushProvider = getCurrentPushProvider() ?: return false val config = pushProvider.getCurrentUserPushConfig() ?: return false - pushersManager.testPush(config) + testPush.execute(config) return true } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt similarity index 79% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt index 7dba9f56780..481081de1f9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriber.kt @@ -22,13 +22,9 @@ import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData -import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest -import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret @@ -37,29 +33,14 @@ import javax.inject.Inject internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" -private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag) +private val loggerTag = LoggerTag("DefaultPusherSubscriber", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) -class PushersManager @Inject constructor( - // private val localeProvider: LocaleProvider, +class DefaultPusherSubscriber @Inject constructor( private val buildMeta: BuildMeta, - // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, - private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, private val pushClientSecret: PushClientSecret, private val userPushStoreFactory: UserPushStoreFactory, ) : PusherSubscriber { - suspend fun testPush(config: CurrentUserPushConfig) { - pushGatewayNotifyRequest.execute( - PushGatewayNotifyRequest.Params( - url = config.url, - appId = PushConfig.PUSHER_APP_ID, - pushKey = config.pushKey, - eventId = TEST_EVENT_ID, - roomId = TEST_ROOM_ID, - ) - ) - } - /** * Register a pusher to the server if not done yet. */ @@ -131,9 +112,4 @@ class PushersManager @Inject constructor( Timber.tag(loggerTag.value).e(throwable, "Unable to unregister the pusher") } } - - companion object { - val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID") - val TEST_ROOM_ID = RoomId("!room:domain") - } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt similarity index 96% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index edc773b89b4..0ab01937f4b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -19,7 +19,9 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import android.net.Uri import androidx.core.content.FileProvider +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider @@ -55,7 +57,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.NotificationLoggerTag) +private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.NotificationLoggerTag) /** * The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event. @@ -63,15 +65,20 @@ private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.Notificat * The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that, * this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk. */ -class NotifiableEventResolver @Inject constructor( +interface NotifiableEventResolver { + suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? +} + +@ContributesBinding(AppScope::class) +class DefaultNotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, private val clock: SystemClock, private val matrixClientProvider: MatrixClientProvider, private val notificationMediaRepoFactory: NotificationMediaRepo.Factory, @ApplicationContext private val context: Context, private val permalinkParser: PermalinkParser, -) { - suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { +) : NotifiableEventResolver { + override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { // Restore session val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val notificationService = client.notificationService() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index 7c9a60c2792..f4cd1bab39c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -221,18 +221,6 @@ class DefaultNotificationDrawerManager @Inject constructor( } } - // TODO EAx Must be per account - fun notificationStyleChanged() { - updateEvents(doRender = true) { - val newSettings = true // pushDataStore.useCompleteNotificationFormat() - if (newSettings != useCompleteNotificationFormat) { - // Settings has changed, remove all current notifications - notificationRenderer.cancelAllNotifications() - useCompleteNotificationFormat = newSettings - } - } - } - private fun updateEvents( doRender: Boolean, action: (NotificationEventQueue) -> Unit, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 2c9abb77b3a..1d3f349364c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -16,27 +16,19 @@ package io.element.android.libraries.push.impl.push -import android.os.Handler -import android.os.Looper import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver -import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -44,23 +36,15 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag) @ContributesBinding(AppScope::class) class DefaultPushHandler @Inject constructor( - private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, + private val onNotifiableEventReceived: OnNotifiableEventReceived, private val notifiableEventResolver: NotifiableEventResolver, - private val defaultPushDataStore: DefaultPushDataStore, + private val incrementPushDataStore: IncrementPushDataStore, private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, - // private val actionIds: NotificationActionIds, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, private val diagnosticPushHandler: DiagnosticPushHandler, ) : PushHandler { - private val coroutineScope = CoroutineScope(SupervisorJob()) - - // UI handler - private val uiHandler by lazy { - Handler(Looper.getMainLooper()) - } - /** * Called when message is received. * @@ -68,21 +52,15 @@ class DefaultPushHandler @Inject constructor( */ override suspend fun handle(pushData: PushData) { Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}") - if (buildMeta.lowPrivacyLoggingEnabled) { Timber.tag(loggerTag.value).d("## pushData: $pushData") } - - defaultPushDataStore.incrementPushCounter() - + incrementPushDataStore.incrementPushCounter() // Diagnostic Push - if (pushData.eventId == PushersManager.TEST_EVENT_ID) { + if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) { diagnosticPushHandler.handlePush() - return - } - - uiHandler.post { - coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } + } else { + handleInternal(pushData) } } @@ -98,7 +76,6 @@ class DefaultPushHandler @Inject constructor( } else { Timber.tag(loggerTag.value).d("## handleInternal()") } - val clientSecret = pushData.clientSecret // clientSecret should not be null. If this happens, restore default session val userId = clientSecret @@ -109,27 +86,22 @@ class DefaultPushHandler @Inject constructor( ?: run { matrixAuthenticationService.getLatestSessionId() } - if (userId == null) { Timber.w("Unable to get a session") return } - - val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) - - if (notifiableEvent == null) { - Timber.w("Unable to get a notification data") - return - } - val userPushStore = userPushStoreFactory.getOrCreate(userId) - if (!userPushStore.getNotificationEnabledForDevice().first()) { + if (userPushStore.getNotificationEnabledForDevice().first()) { + val notifiableEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId) + if (notifiableEvent == null) { + Timber.w("Unable to get a notification data") + return + } + onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent) + } else { // TODO We need to check if this is an incoming call Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") - return } - - defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt new file mode 100644 index 00000000000..9a7f99176c8 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/IncrementPushDataStore.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.push + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import javax.inject.Inject + +interface IncrementPushDataStore { + suspend fun incrementPushCounter() +} + +@ContributesBinding(AppScope::class) +class DefaultIncrementPushDataStore @Inject constructor( + private val defaultPushDataStore: DefaultPushDataStore +) : IncrementPushDataStore { + override suspend fun incrementPushCounter() { + defaultPushDataStore.incrementPushCounter() + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt new file mode 100644 index 00000000000..6d4ad34eb87 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/OnNotifiableEventReceived.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.push + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import javax.inject.Inject + +interface OnNotifiableEventReceived { + fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) +} + +@ContributesBinding(AppScope::class) +class DefaultOnNotifiableEventReceived @Inject constructor( + private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, +) : OnNotifiableEventReceived { + override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt index d8de5429efa..4df39587f14 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayAPI.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.pushgateway import retrofit2.http.Body import retrofit2.http.POST -internal interface PushGatewayAPI { +interface PushGatewayAPI { /** * Ask the Push Gateway to send a push to the current device. * diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt new file mode 100644 index 00000000000..1ea4c72fbba --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayApiFactory.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.pushgateway + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.network.RetrofitFactory +import javax.inject.Inject + +interface PushGatewayApiFactory { + fun create(baseUrl: String): PushGatewayAPI +} + +@ContributesBinding(AppScope::class) +class DefaultPushGatewayApiFactory @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) : PushGatewayApiFactory { + override fun create(baseUrl: String): PushGatewayAPI { + return retrofitFactory.create(baseUrl) + .create(PushGatewayAPI::class.java) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt index 7adedfcfd26..ad5a2641681 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayDevice.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -internal data class PushGatewayDevice( +data class PushGatewayDevice( /** * Required. The app_id given when the pusher was created. */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt index 5e341e3286e..28ad04a078c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotification.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -internal data class PushGatewayNotification( +data class PushGatewayNotification( @SerialName("event_id") val eventId: String, @SerialName("room_id") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt index ce41d2d83e9..14727cab2f5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyBody.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -internal data class PushGatewayNotifyBody( +data class PushGatewayNotifyBody( /** * Required. Information about the push notification */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt index e8c01493ab3..41c8a05423f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyRequest.kt @@ -15,15 +15,14 @@ */ package io.element.android.libraries.push.impl.pushgateway +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.push.api.gateway.PushGatewayFailure import javax.inject.Inject -class PushGatewayNotifyRequest @Inject constructor( - private val retrofitFactory: RetrofitFactory, -) { +interface PushGatewayNotifyRequest { data class Params( val url: String, val appId: String, @@ -32,13 +31,18 @@ class PushGatewayNotifyRequest @Inject constructor( val roomId: RoomId, ) - suspend fun execute(params: Params) { - val sygnalApi = retrofitFactory.create( + suspend fun execute(params: Params) +} + +@ContributesBinding(AppScope::class) +class DefaultPushGatewayNotifyRequest @Inject constructor( + private val pushGatewayApiFactory: PushGatewayApiFactory, +) : PushGatewayNotifyRequest { + override suspend fun execute(params: PushGatewayNotifyRequest.Params) { + val pushGatewayApi = pushGatewayApiFactory.create( params.url.substringBefore(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH) ) - .create(PushGatewayAPI::class.java) - - val response = sygnalApi.notify( + val response = pushGatewayApi.notify( PushGatewayNotifyBody( PushGatewayNotification( eventId = params.eventId.value, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt index 13d9cbad1de..75b5e521110 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/pushgateway/PushGatewayNotifyResponse.kt @@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -internal data class PushGatewayNotifyResponse( +data class PushGatewayNotifyResponse( @SerialName("rejected") val rejectedPushKeys: List ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt new file mode 100644 index 00000000000..667918941ed --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/test/TestPush.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.test + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.appconfig.PushConfig +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import javax.inject.Inject + +interface TestPush { + suspend fun execute(config: CurrentUserPushConfig) +} + +@ContributesBinding(AppScope::class) +class DefaultTestPush @Inject constructor( + private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, +) : TestPush { + override suspend fun execute(config: CurrentUserPushConfig) { + pushGatewayNotifyRequest.execute( + PushGatewayNotifyRequest.Params( + url = config.url, + appId = PushConfig.PUSHER_APP_ID, + pushKey = config.pushKey, + eventId = TEST_EVENT_ID, + roomId = TEST_ROOM_ID, + ) + ) + } + + companion object { + val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID") + val TEST_ROOM_ID = RoomId("!room:domain") + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt new file mode 100644 index 00000000000..546d69b0081 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.push.impl.test.FakeTestPush +import io.element.android.libraries.push.impl.test.TestPush +import io.element.android.libraries.push.test.FakeGetCurrentPushProvider +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.libraries.pushproviders.test.FakePushProvider +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPushServiceTest { + @Test + fun `test push no push provider`() = runTest { + val defaultPushService = createDefaultPushService() + assertThat(defaultPushService.testPush()).isFalse() + } + + @Test + fun `test push no config`() = runTest { + val aPushProvider = FakePushProvider() + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aPushProvider), + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name), + ) + assertThat(defaultPushService.testPush()).isFalse() + } + + @Test + fun `test push ok`() = runTest { + val aConfig = CurrentUserPushConfig( + url = "aUrl", + pushKey = "aPushKey", + ) + val testPushResult = lambdaRecorder { } + val aPushProvider = FakePushProvider( + currentUserPushConfig = aConfig + ) + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aPushProvider), + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name), + testPush = FakeTestPush(executeResult = testPushResult), + ) + assertThat(defaultPushService.testPush()).isTrue() + testPushResult.assertions() + .isCalledOnce() + .with(value(aConfig)) + } + + @Test + fun `getCurrentPushProvider null`() = runTest { + val defaultPushService = createDefaultPushService() + val result = defaultPushService.getCurrentPushProvider() + assertThat(result).isNull() + } + + @Test + fun `getCurrentPushProvider ok`() = runTest { + val aPushProvider = FakePushProvider() + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aPushProvider), + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aPushProvider.name), + ) + val result = defaultPushService.getCurrentPushProvider() + assertThat(result).isEqualTo(aPushProvider) + } + + @Test + fun `getAvailablePushProviders empty`() = runTest { + val defaultPushService = createDefaultPushService() + val result = defaultPushService.getAvailablePushProviders() + assertThat(result).isEmpty() + } + + @Test + fun `registerWith ok`() = runTest { + val client = FakeMatrixClient() + val aPushProvider = FakePushProvider( + registerWithResult = { _, _ -> Result.success(Unit) }, + ) + val aDistributor = Distributor("aValue", "aName") + val defaultPushService = createDefaultPushService() + val result = defaultPushService.registerWith(client, aPushProvider, aDistributor) + assertThat(result).isEqualTo(Result.success(Unit)) + } + + @Test + fun `registerWith fail to register`() = runTest { + val client = FakeMatrixClient() + val aPushProvider = FakePushProvider( + registerWithResult = { _, _ -> Result.failure(AN_EXCEPTION) }, + ) + val aDistributor = Distributor("aValue", "aName") + val defaultPushService = createDefaultPushService() + val result = defaultPushService.registerWith(client, aPushProvider, aDistributor) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `registerWith fail to unregister previous push provider`() = runTest { + val client = FakeMatrixClient() + val aCurrentPushProvider = FakePushProvider( + unregisterWithResult = { Result.failure(AN_EXCEPTION) }, + name = "aCurrentPushProvider", + ) + val aPushProvider = FakePushProvider( + name = "aPushProvider", + ) + val userPushStore = FakeUserPushStore().apply { + setPushProviderName(aCurrentPushProvider.name) + } + val aDistributor = Distributor("aValue", "aName") + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aCurrentPushProvider, aPushProvider), + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aCurrentPushProvider.name), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { userPushStore }, + ), + ) + val result = defaultPushService.registerWith(client, aPushProvider, aDistributor) + assertThat(result.isFailure).isTrue() + assertThat(userPushStore.getPushProviderName()).isEqualTo(aCurrentPushProvider.name) + } + + @Test + fun `registerWith unregister previous push provider and register new OK`() = runTest { + val client = FakeMatrixClient() + val unregisterLambda = lambdaRecorder> { Result.success(Unit) } + val registerLambda = lambdaRecorder> { _, _ -> Result.success(Unit) } + val aCurrentPushProvider = FakePushProvider( + unregisterWithResult = unregisterLambda, + name = "aCurrentPushProvider", + ) + val aPushProvider = FakePushProvider( + registerWithResult = registerLambda, + name = "aPushProvider", + ) + val userPushStore = FakeUserPushStore().apply { + setPushProviderName(aCurrentPushProvider.name) + } + val aDistributor = Distributor("aValue", "aName") + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aCurrentPushProvider, aPushProvider), + getCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = aCurrentPushProvider.name), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { userPushStore }, + ), + ) + val result = defaultPushService.registerWith(client, aPushProvider, aDistributor) + assertThat(result.isSuccess).isTrue() + assertThat(userPushStore.getPushProviderName()).isEqualTo(aPushProvider.name) + unregisterLambda.assertions() + .isCalledOnce() + .with(value(client)) + registerLambda.assertions() + .isCalledOnce() + .with(value(client), value(aDistributor)) + } + + @Test + fun `getAvailablePushProviders sorted`() = runTest { + val aPushProvider1 = FakePushProvider( + index = 1, + name = "aPushProvider1", + ) + val aPushProvider2 = FakePushProvider( + index = 2, + name = "aPushProvider2", + ) + val aPushProvider3 = FakePushProvider( + index = 3, + name = "aPushProvider3", + ) + val defaultPushService = createDefaultPushService( + pushProviders = setOf(aPushProvider1, aPushProvider3, aPushProvider2), + ) + val result = defaultPushService.getAvailablePushProviders() + assertThat(result).containsExactly(aPushProvider1, aPushProvider2, aPushProvider3).inOrder() + } + + private fun createDefaultPushService( + testPush: TestPush = FakeTestPush(), + userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), + pushProviders: Set<@JvmSuppressWildcards PushProvider> = emptySet(), + getCurrentPushProvider: GetCurrentPushProvider = FakeGetCurrentPushProvider(currentPushProvider = null), + ): DefaultPushService { + return DefaultPushService( + testPush = testPush, + userPushStoreFactory = userPushStoreFactory, + pushProviders = pushProviders, + getCurrentPushProvider = getCurrentPushProvider, + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt new file mode 100644 index 00000000000..dd9363de9e3 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPusherSubscriberTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.appconfig.PushConfig +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData +import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.matrix.test.pushers.FakePushersService +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPusherSubscriberTest { + @Test + fun `test register pusher OK`() = runTest { + testRegisterPusher( + currentPushKey = null, + registerResult = Result.success(Unit), + ) + } + + @Test + fun `test re-register pusher OK`() = runTest { + testRegisterPusher( + currentPushKey = "aPushKey", + registerResult = Result.success(Unit), + ) + } + + @Test + fun `test register pusher error`() = runTest { + testRegisterPusher( + currentPushKey = null, + registerResult = Result.failure(AN_EXCEPTION), + ) + } + + @Test + fun `test re-register pusher error`() = runTest { + testRegisterPusher( + currentPushKey = "aPushKey", + registerResult = Result.failure(AN_EXCEPTION), + ) + } + + private suspend fun testRegisterPusher( + currentPushKey: String?, + registerResult: Result, + ) { + val setHttpPusherResult = lambdaRecorder> { registerResult } + val userPushStore = FakeUserPushStore().apply { + setCurrentRegisteredPushKey(currentPushKey) + } + assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(currentPushKey) + + val matrixClient = FakeMatrixClient( + pushersService = FakePushersService( + setHttpPusherResult = setHttpPusherResult, + ), + ) + val defaultPusherSubscriber = createDefaultPusherSubscriber( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = { A_SECRET }, + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { userPushStore }, + ), + ) + val result = defaultPusherSubscriber.registerPusher( + matrixClient = matrixClient, + pushKey = "aPushKey", + gateway = "aGateway", + ) + assertThat(result).isEqualTo(registerResult) + setHttpPusherResult.assertions() + .isCalledOnce() + .with( + value( + SetHttpPusherData( + pushKey = "aPushKey", + appId = PushConfig.PUSHER_APP_ID, + url = "aGateway", + appDisplayName = "MyApp", + deviceDisplayName = "MyDevice", + profileTag = DEFAULT_PUSHER_FILE_TAG + "_", + lang = "en", + defaultPayload = "{\"cs\":\"$A_SECRET\"}", + ), + ) + ) + assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo( + if (registerResult.isSuccess) "aPushKey" else currentPushKey + ) + } + + @Test + fun `test unregister pusher OK`() = runTest { + testUnregisterPusher( + currentPushKey = "aPushKey", + unregisterResult = Result.success(Unit), + ) + } + + @Test + fun `test unregister pusher error`() = runTest { + testUnregisterPusher( + currentPushKey = "aPushKey", + unregisterResult = Result.failure(AN_EXCEPTION), + ) + } + + private suspend fun testUnregisterPusher( + currentPushKey: String?, + unregisterResult: Result, + ) { + val unsetHttpPusherResult = lambdaRecorder> { unregisterResult } + val userPushStore = FakeUserPushStore().apply { + setCurrentRegisteredPushKey(currentPushKey) + } + assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo(currentPushKey) + + val matrixClient = FakeMatrixClient( + pushersService = FakePushersService( + unsetHttpPusherResult = unsetHttpPusherResult, + ), + ) + val defaultPusherSubscriber = createDefaultPusherSubscriber( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = { A_SECRET }, + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { userPushStore }, + ), + ) + val result = defaultPusherSubscriber.unregisterPusher( + matrixClient = matrixClient, + pushKey = "aPushKey", + gateway = "aGateway", + ) + assertThat(result).isEqualTo(unregisterResult) + unsetHttpPusherResult.assertions() + .isCalledOnce() + .with( + value( + UnsetHttpPusherData( + pushKey = "aPushKey", + appId = PushConfig.PUSHER_APP_ID, + ), + ) + ) + assertThat(userPushStore.getCurrentRegisteredPushKey()).isEqualTo( + if (unregisterResult.isSuccess) null else currentPushKey + ) + } + + private fun createDefaultPusherSubscriber( + buildMeta: BuildMeta = aBuildMeta(applicationName = "MyApp"), + userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), + pushClientSecret: PushClientSecret = FakePushClientSecret(), + ): DefaultPusherSubscriber { + return DefaultPusherSubscriber( + buildMeta = buildMeta, + pushClientSecret = pushClientSecret, + userPushStoreFactory = userPushStoreFactory, + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt similarity index 94% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt rename to libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index d0cd5a30e52..8eebbbbb5cc 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -59,17 +59,17 @@ import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) -class NotifiableEventResolverTest { +class DefaultNotifiableEventResolverTest { @Test fun `resolve event no session`() = runTest { - val sut = createNotifiableEventResolver(notificationService = null) + val sut = createDefaultNotifiableEventResolver(notificationService = null) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) assertThat(result).isNull() } @Test fun `resolve event failure`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.failure(AN_EXCEPTION) ) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) @@ -78,7 +78,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event null`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success(null) ) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) @@ -87,7 +87,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message text`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -105,7 +105,7 @@ class NotifiableEventResolverTest { @Test @Config(qualifiers = "en") fun `resolve event message with mention`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -123,7 +123,7 @@ class NotifiableEventResolverTest { @Test fun `resolve HTML formatted event message text takes plain text version`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -146,7 +146,7 @@ class NotifiableEventResolverTest { @Test fun `resolve incorrectly formatted event message text uses fallback`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -169,7 +169,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message audio`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -186,7 +186,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message video`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -203,7 +203,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message voice`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -220,7 +220,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message image`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -237,7 +237,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message sticker`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -254,7 +254,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message file`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -271,7 +271,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message location`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -288,7 +288,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message notice`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -305,7 +305,7 @@ class NotifiableEventResolverTest { @Test fun `resolve event message emote`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomMessage( @@ -322,7 +322,7 @@ class NotifiableEventResolverTest { @Test fun `resolve poll`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.Poll( @@ -339,7 +339,7 @@ class NotifiableEventResolverTest { @Test fun `resolve RoomMemberContent invite room`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.StateEvent.RoomMemberContent( @@ -372,7 +372,7 @@ class NotifiableEventResolverTest { @Test fun `resolve RoomMemberContent invite direct`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.StateEvent.RoomMemberContent( @@ -405,7 +405,7 @@ class NotifiableEventResolverTest { @Test fun `resolve RoomMemberContent other`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.StateEvent.RoomMemberContent( @@ -421,7 +421,7 @@ class NotifiableEventResolverTest { @Test fun `resolve RoomEncrypted`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.RoomEncrypted @@ -445,7 +445,7 @@ class NotifiableEventResolverTest { @Test fun `resolve CallInvite`() = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2) @@ -517,7 +517,7 @@ class NotifiableEventResolverTest { } private fun testNull(content: NotificationContent) = runTest { - val sut = createNotifiableEventResolver( + val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( createNotificationData( content = content @@ -528,10 +528,10 @@ class NotifiableEventResolverTest { assertThat(result).isNull() } - private fun createNotifiableEventResolver( + private fun createDefaultNotifiableEventResolver( notificationService: FakeNotificationService? = FakeNotificationService(), notificationResult: Result = Result.success(null), - ): NotifiableEventResolver { + ): DefaultNotifiableEventResolver { val context = RuntimeEnvironment.getApplication() as Context notificationService?.givenGetNotificationResult(notificationResult) val matrixClientProvider = FakeMatrixClientProvider(getClient = { @@ -544,7 +544,7 @@ class NotifiableEventResolverTest { val notificationMediaRepoFactory = NotificationMediaRepo.Factory { FakeNotificationMediaRepo() } - return NotifiableEventResolver( + return DefaultNotifiableEventResolver( stringProvider = AndroidStringProvider(context.resources), clock = FakeSystemClock(), matrixClientProvider = matrixClientProvider, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index b84e7b4be32..e3220f8dfa1 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -59,7 +59,6 @@ class DefaultNotificationDrawerManagerTest { fun `cover all APIs`() = runTest { // For now just call all the API. Later, add more valuable tests. val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() - defaultNotificationDrawerManager.notificationStyleChanged() defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true) defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false) defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt new file mode 100644 index 00000000000..a9ccabaa6d6 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FakeNotifiableEventResolver.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeNotifiableEventResolver( + private val notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() } +) : NotifiableEventResolver { + override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { + return notifiableEventResult(sessionId, roomId, eventId) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt new file mode 100644 index 00000000000..bae296a574e --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.push.impl.push + +import app.cash.turbine.test +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver +import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.libraries.push.impl.test.DefaultTestPush +import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler +import io.element.android.libraries.pushproviders.api.PushData +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultPushHandlerTest { + @Test + fun `when classical PushData is received, the notification drawer is informed`() = runTest { + val aNotifiableMessageEvent = aNotifiableMessageEvent() + val notifiableEventResult = + lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + val onNotifiableEventReceived = lambdaRecorder {} + val incrementPushCounterResult = lambdaRecorder {} + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = notifiableEventResult, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + incrementPushCounterResult = incrementPushCounterResult + ) + defaultPushHandler.handle(aPushData) + incrementPushCounterResult.assertions() + .isCalledOnce() + notifiableEventResult.assertions() + .isCalledOnce() + .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) + onNotifiableEventReceived.assertions() + .isCalledOnce() + .with(value(aNotifiableMessageEvent)) + } + + @Test + fun `when classical PushData is received, but notifications are disabled, nothing happen`() = + runTest { + val aNotifiableMessageEvent = aNotifiableMessageEvent() + val notifiableEventResult = + lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + val onNotifiableEventReceived = lambdaRecorder {} + val incrementPushCounterResult = lambdaRecorder {} + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = notifiableEventResult, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + userPushStore = FakeUserPushStore().apply { + setNotificationEnabledForDevice(false) + }, + incrementPushCounterResult = incrementPushCounterResult + ) + defaultPushHandler.handle(aPushData) + incrementPushCounterResult.assertions() + .isCalledOnce() + notifiableEventResult.assertions() + .isNeverCalled() + onNotifiableEventReceived.assertions() + .isNeverCalled() + } + + @Test + fun `when PushData is received, but client secret is not known, fallback the latest session`() = + runTest { + val aNotifiableMessageEvent = aNotifiableMessageEvent() + val notifiableEventResult = + lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + val onNotifiableEventReceived = lambdaRecorder {} + val incrementPushCounterResult = lambdaRecorder {} + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = notifiableEventResult, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { null } + ), + matrixAuthenticationService = FakeAuthenticationService().apply { + getLatestSessionIdLambda = { A_USER_ID } + }, + incrementPushCounterResult = incrementPushCounterResult + ) + defaultPushHandler.handle(aPushData) + incrementPushCounterResult.assertions() + .isCalledOnce() + notifiableEventResult.assertions() + .isCalledOnce() + .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) + onNotifiableEventReceived.assertions() + .isCalledOnce() + .with(value(aNotifiableMessageEvent)) + } + + @Test + fun `when PushData is received, but client secret is not known, and there is no latest session, nothing happen`() = + runTest { + val aNotifiableMessageEvent = aNotifiableMessageEvent() + val notifiableEventResult = + lambdaRecorder { _, _, _ -> aNotifiableMessageEvent } + val onNotifiableEventReceived = lambdaRecorder {} + val incrementPushCounterResult = lambdaRecorder {} + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = notifiableEventResult, + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { null } + ), + matrixAuthenticationService = FakeAuthenticationService().apply { + getLatestSessionIdLambda = { null } + }, + incrementPushCounterResult = incrementPushCounterResult + ) + defaultPushHandler.handle(aPushData) + incrementPushCounterResult.assertions() + .isCalledOnce() + notifiableEventResult.assertions() + .isNeverCalled() + onNotifiableEventReceived.assertions() + .isNeverCalled() + } + + @Test + fun `when classical PushData is received, but not able to resolve the event, nothing happen`() = + runTest { + val notifiableEventResult = + lambdaRecorder { _, _, _ -> null } + val onNotifiableEventReceived = lambdaRecorder {} + val incrementPushCounterResult = lambdaRecorder {} + val aPushData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val defaultPushHandler = createDefaultPushHandler( + onNotifiableEventReceived = onNotifiableEventReceived, + notifiableEventResult = notifiableEventResult, + buildMeta = aBuildMeta( + // Also test `lowPrivacyLoggingEnabled = false` here + lowPrivacyLoggingEnabled = false + ), + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + incrementPushCounterResult = incrementPushCounterResult + ) + defaultPushHandler.handle(aPushData) + incrementPushCounterResult.assertions() + .isCalledOnce() + notifiableEventResult.assertions() + .isCalledOnce() + .with(value(A_USER_ID), value(A_ROOM_ID), value(AN_EVENT_ID)) + onNotifiableEventReceived.assertions() + .isNeverCalled() + } + + @Test + fun `when diagnostic PushData is received, the diagnostic push handler is informed `() = + runTest { + val aPushData = PushData( + eventId = DefaultTestPush.TEST_EVENT_ID, + roomId = A_ROOM_ID, + unread = 0, + clientSecret = A_SECRET, + ) + val diagnosticPushHandler = DiagnosticPushHandler() + val defaultPushHandler = createDefaultPushHandler( + diagnosticPushHandler = diagnosticPushHandler, + incrementPushCounterResult = { } + ) + diagnosticPushHandler.state.test { + defaultPushHandler.handle(aPushData) + awaitItem() + } + } + + private fun createDefaultPushHandler( + onNotifiableEventReceived: (NotifiableEvent) -> Unit = { lambdaError() }, + notifiableEventResult: (SessionId, RoomId, EventId) -> NotifiableEvent? = { _, _, _ -> lambdaError() }, + incrementPushCounterResult: () -> Unit = { lambdaError() }, + userPushStore: UserPushStore = FakeUserPushStore(), + pushClientSecret: PushClientSecret = FakePushClientSecret(), + buildMeta: BuildMeta = aBuildMeta(), + matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(), + diagnosticPushHandler: DiagnosticPushHandler = DiagnosticPushHandler(), + ): DefaultPushHandler { + return DefaultPushHandler( + onNotifiableEventReceived = FakeOnNotifiableEventReceived(onNotifiableEventReceived), + notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventResult), + incrementPushDataStore = object : IncrementPushDataStore { + override suspend fun incrementPushCounter() { + incrementPushCounterResult() + } + }, + userPushStoreFactory = FakeUserPushStoreFactory { userPushStore }, + pushClientSecret = pushClientSecret, + buildMeta = buildMeta, + matrixAuthenticationService = matrixAuthenticationService, + diagnosticPushHandler = diagnosticPushHandler, + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt new file mode 100644 index 00000000000..3c9e025830a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeOnNotifiableEventReceived.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.push + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent + +class FakeOnNotifiableEventReceived( + private val onNotifiableEventReceivedResult: (NotifiableEvent) -> Unit, +) : OnNotifiableEventReceived { + override fun onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { + onNotifiableEventReceivedResult(notifiableEvent) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/DefaultPushGatewayNotifyRequestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/DefaultPushGatewayNotifyRequestTest.kt new file mode 100644 index 00000000000..ba432b4c7a9 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/DefaultPushGatewayNotifyRequestTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.pushgateway + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.push.api.gateway.PushGatewayFailure +import io.element.android.libraries.push.impl.test.DefaultTestPush +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertThrows +import org.junit.Test + +class DefaultPushGatewayNotifyRequestTest { + @Test + fun `notify success`() = runTest { + val factory = FakePushGatewayApiFactory( + notifyResponse = { + PushGatewayNotifyResponse( + rejectedPushKeys = emptyList() + ) + } + ) + val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest( + pushGatewayApiFactory = factory, + ) + pushGatewayNotifyRequest.execute( + PushGatewayNotifyRequest.Params( + url = "aUrl", + appId = "anAppId", + pushKey = "aPushKey", + eventId = DefaultTestPush.TEST_EVENT_ID, + roomId = DefaultTestPush.TEST_ROOM_ID, + ) + ) + assertThat(factory.baseUrlParameter).isEqualTo("aUrl") + } + + @Test + fun `notify success, url is stripped`() = runTest { + val factory = FakePushGatewayApiFactory( + notifyResponse = { + PushGatewayNotifyResponse( + rejectedPushKeys = emptyList() + ) + } + ) + val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest( + pushGatewayApiFactory = factory, + ) + pushGatewayNotifyRequest.execute( + PushGatewayNotifyRequest.Params( + url = "aUrl" + PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH, + appId = "anAppId", + pushKey = "aPushKey", + eventId = DefaultTestPush.TEST_EVENT_ID, + roomId = DefaultTestPush.TEST_ROOM_ID, + ) + ) + assertThat(factory.baseUrlParameter).isEqualTo("aUrl") + } + + @Test + fun `notify with rejected push key should throw expected Exception`() { + val factory = FakePushGatewayApiFactory( + notifyResponse = { + PushGatewayNotifyResponse( + rejectedPushKeys = listOf("aPushKey") + ) + } + ) + val pushGatewayNotifyRequest = DefaultPushGatewayNotifyRequest( + pushGatewayApiFactory = factory, + ) + assertThrows(PushGatewayFailure.PusherRejected::class.java) { + runTest { + pushGatewayNotifyRequest.execute( + PushGatewayNotifyRequest.Params( + url = "aUrl", + appId = "anAppId", + pushKey = "aPushKey", + eventId = DefaultTestPush.TEST_EVENT_ID, + roomId = DefaultTestPush.TEST_ROOM_ID, + ) + ) + } + } + assertThat(factory.baseUrlParameter).isEqualTo("aUrl") + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/FakePushGatewayApiFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/FakePushGatewayApiFactory.kt new file mode 100644 index 00000000000..0b9730843e3 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/pushgateway/FakePushGatewayApiFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.pushgateway + +class FakePushGatewayApiFactory( + private val notifyResponse: () -> PushGatewayNotifyResponse +) : PushGatewayApiFactory { + var baseUrlParameter: String? = null + private set + + override fun create(baseUrl: String): PushGatewayAPI { + baseUrlParameter = baseUrl + return FakePushGatewayAPI(notifyResponse) + } +} + +class FakePushGatewayAPI( + private val notifyResponse: () -> PushGatewayNotifyResponse +) : PushGatewayAPI { + override suspend fun notify(body: PushGatewayNotifyBody): PushGatewayNotifyResponse { + return notifyResponse() + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt new file mode 100644 index 00000000000..0ccd08df826 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/DefaultTestPushTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.test + +import io.element.android.appconfig.PushConfig +import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultTestPushTest { + @Test + fun `test DefaultTestPush`() = runTest { + val executeResult = lambdaRecorder { } + val defaultTestPush = DefaultTestPush( + pushGatewayNotifyRequest = FakePushGatewayNotifyRequest( + executeResult = executeResult, + ) + ) + val aConfig = CurrentUserPushConfig( + url = "aUrl", + pushKey = "aPushKey", + ) + defaultTestPush.execute(aConfig) + executeResult.assertions() + .isCalledOnce() + .with( + value( + PushGatewayNotifyRequest.Params( + url = aConfig.url, + appId = PushConfig.PUSHER_APP_ID, + pushKey = aConfig.pushKey, + eventId = DefaultTestPush.TEST_EVENT_ID, + roomId = DefaultTestPush.TEST_ROOM_ID, + ) + ) + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakePushGatewayNotifyRequest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakePushGatewayNotifyRequest.kt new file mode 100644 index 00000000000..d0fa5a546fe --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakePushGatewayNotifyRequest.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.test + +import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePushGatewayNotifyRequest( + private val executeResult: (PushGatewayNotifyRequest.Params) -> Unit = { lambdaError() } +) : PushGatewayNotifyRequest { + override suspend fun execute(params: PushGatewayNotifyRequest.Params) { + executeResult(params) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakeTestPush.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakeTestPush.kt new file mode 100644 index 00000000000..d7bf8c8c42a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/test/FakeTestPush.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.test + +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeTestPush( + private val executeResult: (CurrentUserPushConfig) -> Unit = { lambdaError() } +) : TestPush { + override suspend fun execute(config: CurrentUserPushConfig) { + executeResult(config) + } +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt index a148e5f6a2c..584e93d8e13 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt @@ -29,9 +29,6 @@ class FakePushService( Result.success(Unit) }, ) : PushService { - override fun notificationStyleChanged() { - } - override suspend fun getCurrentPushProvider(): PushProvider? { return availablePushProviders.firstOrNull() } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePusherSubscriber.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePusherSubscriber.kt new file mode 100644 index 00000000000..8338bb1e4c4 --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePusherSubscriber.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePusherSubscriber( + private val registerPusherResult: (MatrixClient, String, String) -> Result = { _, _, _ -> lambdaError() }, + private val unregisterPusherResult: (MatrixClient, String, String) -> Result = { _, _, _ -> lambdaError() }, +) : PusherSubscriber { + override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result { + return registerPusherResult(matrixClient, pushKey, gateway) + } + + override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result { + return unregisterPusherResult(matrixClient, pushKey, gateway) + } +} diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt new file mode 100644 index 00000000000..c370250bd0f --- /dev/null +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/test/FakePushHandler.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.test.test + +import io.element.android.libraries.pushproviders.api.PushData +import io.element.android.libraries.pushproviders.api.PushHandler +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePushHandler( + private val handleResult: (PushData) -> Unit = { lambdaError() } +) : PushHandler { + override suspend fun handle(pushData: PushData) { + handleResult(pushData) + } +} diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 6e36b92a097..58d3b882d94 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -58,7 +58,11 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.push.test) + testImplementation(projects.libraries.pushstore.test) + testImplementation(projects.libraries.sessionStorage.implMemory) testImplementation(projects.tests.testutils) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt index e61ea4bc912..baddab1a0d9 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/FirebaseNewTokenHandler.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.pushproviders.firebase +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushproviders.api.PusherSubscriber @@ -32,14 +34,19 @@ private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLogge /** * Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider. */ -class FirebaseNewTokenHandler @Inject constructor( +interface FirebaseNewTokenHandler { + suspend fun handle(firebaseToken: String) +} + +@ContributesBinding(AppScope::class) +class DefaultFirebaseNewTokenHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, private val matrixAuthenticationService: MatrixAuthenticationService, private val firebaseStore: FirebaseStore, -) { - suspend fun handle(firebaseToken: String) { +) : FirebaseNewTokenHandler { + override suspend fun handle(firebaseToken: String) { firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList() @@ -53,14 +60,15 @@ class FirebaseNewTokenHandler @Inject constructor( Timber.tag(loggerTag.value).e(it, "Failed to restore session $sessionId") } .flatMap { client -> - pusherSubscriber.registerPusher( - matrixClient = client, - pushKey = firebaseToken, - gateway = FirebaseConfig.PUSHER_HTTP_URL, - ) - } - .onFailure { - Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId") + pusherSubscriber + .registerPusher( + matrixClient = client, + pushKey = firebaseToken, + gateway = FirebaseConfig.PUSHER_HTTP_URL, + ) + .onFailure { + Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId") + } } } else { Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt index 3d251f6e64e..a8bc0698934 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingService.kt @@ -22,7 +22,6 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.pushproviders.api.PushHandler import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -33,8 +32,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler - - private val coroutineScope = CoroutineScope(SupervisorJob()) + @Inject lateinit var coroutineScope: CoroutineScope override fun onCreate() { super.onCreate() diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt new file mode 100644 index 00000000000..af2b98ea07c --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/DefaultFirebaseNewTokenHandlerTest.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.libraries.push.test.FakePusherSubscriber +import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.libraries.sessionstorage.api.LoginType +import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.impl.memory.InMemoryMultiSessionsStore +import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultFirebaseNewTokenHandlerTest { + @Test + fun `when a new token is received it is stored in the firebase store`() = runTest { + val firebaseStore = InMemoryFirebaseStore() + assertThat(firebaseStore.getFcmToken()).isNull() + val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( + firebaseStore = firebaseStore, + ) + firebaseNewTokenHandler.handle("aToken") + assertThat(firebaseStore.getFcmToken()).isEqualTo("aToken") + } + + @Test + fun `when a new token is received, the handler registers the pusher again to all sessions using Firebase`() = runTest { + val aMatrixClient1 = FakeMatrixClient(A_USER_ID) + val aMatrixClient2 = FakeMatrixClient(A_USER_ID_2) + val aMatrixClient3 = FakeMatrixClient(A_USER_ID_3) + val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } + val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) + val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( + sessionStore = InMemoryMultiSessionsStore().apply { + storeData(aSessionData(A_USER_ID)) + storeData(aSessionData(A_USER_ID_2)) + storeData(aSessionData(A_USER_ID_3)) + }, + matrixAuthenticationService = FakeAuthenticationService( + matrixClientResult = { sessionId -> + when (sessionId) { + A_USER_ID -> Result.success(aMatrixClient1) + A_USER_ID_2 -> Result.success(aMatrixClient2) + A_USER_ID_3 -> Result.success(aMatrixClient3) + else -> Result.failure(IllegalStateException()) + } + } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { sessionId -> + when (sessionId) { + A_USER_ID -> FakeUserPushStore(pushProviderName = FirebaseConfig.NAME) + A_USER_ID_2 -> FakeUserPushStore(pushProviderName = "Other") + A_USER_ID_3 -> FakeUserPushStore(pushProviderName = FirebaseConfig.NAME) + else -> error("Unexpected sessionId: $sessionId") + } + } + ), + pusherSubscriber = pusherSubscriber, + ) + firebaseNewTokenHandler.handle("aToken") + registerPusherResult.assertions() + .isCalledExactly(2) + .withSequence( + listOf(value(aMatrixClient1), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)), + listOf(value(aMatrixClient3), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)), + ) + } + + @Test + fun `when a new token is received, if the session cannot be restore, nothing happen`() = runTest { + val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } + val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) + val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( + sessionStore = InMemoryMultiSessionsStore().apply { + storeData(aSessionData(A_USER_ID)) + }, + matrixAuthenticationService = FakeAuthenticationService( + matrixClientResult = { _ -> + Result.failure(IllegalStateException()) + } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { _ -> + FakeUserPushStore(pushProviderName = FirebaseConfig.NAME) + } + ), + pusherSubscriber = pusherSubscriber, + ) + firebaseNewTokenHandler.handle("aToken") + registerPusherResult.assertions() + .isNeverCalled() + } + + @Test + fun `when a new token is received, error when registering the pusher is ignored`() = runTest { + val aMatrixClient1 = FakeMatrixClient(A_USER_ID) + val registerPusherResult = lambdaRecorder> { _, _, _ -> Result.failure(AN_EXCEPTION) } + val pusherSubscriber = FakePusherSubscriber(registerPusherResult = registerPusherResult) + val firebaseNewTokenHandler = createDefaultFirebaseNewTokenHandler( + sessionStore = InMemoryMultiSessionsStore().apply { + storeData(aSessionData(A_USER_ID)) + }, + matrixAuthenticationService = FakeAuthenticationService( + matrixClientResult = { _ -> + Result.success(aMatrixClient1) + } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { _ -> + FakeUserPushStore(pushProviderName = FirebaseConfig.NAME) + } + ), + pusherSubscriber = pusherSubscriber, + ) + firebaseNewTokenHandler.handle("aToken") + registerPusherResult.assertions() + registerPusherResult.assertions() + .isCalledOnce() + .with(value(aMatrixClient1), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + } + + private fun createDefaultFirebaseNewTokenHandler( + pusherSubscriber: PusherSubscriber = FakePusherSubscriber(), + sessionStore: SessionStore = InMemorySessionStore(), + userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), + matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService(), + firebaseStore: FirebaseStore = InMemoryFirebaseStore(), + ): FirebaseNewTokenHandler { + return DefaultFirebaseNewTokenHandler( + pusherSubscriber = pusherSubscriber, + sessionStore = sessionStore, + userPushStoreFactory = userPushStoreFactory, + matrixAuthenticationService = matrixAuthenticationService, + firebaseStore = firebaseStore + ) + } + + private fun aSessionData( + sessionId: SessionId, + ): SessionData { + return SessionData( + userId = sessionId.value, + deviceId = "aDeviceId", + accessToken = "anAccessToken", + refreshToken = "aRefreshToken", + homeserverUrl = "aHomeserverUrl", + oidcData = null, + slidingSyncProxy = null, + loginTimestamp = null, + isTokenValid = true, + loginType = LoginType.UNKNOWN, + passphrase = null, + ) + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt new file mode 100644 index 00000000000..aa66f0288c8 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeFirebaseNewTokenHandler.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeFirebaseNewTokenHandler( + private val handleResult: (String) -> Unit = { lambdaError() } +) : FirebaseNewTokenHandler { + override suspend fun handle(firebaseToken: String) { + handleResult(firebaseToken) + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeIsPlayServiceAvailable.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeIsPlayServiceAvailable.kt new file mode 100644 index 00000000000..6994e6140ee --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FakeIsPlayServiceAvailable.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +class FakeIsPlayServiceAvailable( + private val isAvailable: Boolean, +) : IsPlayServiceAvailable { + override fun isAvailable() = isAvailable +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt new file mode 100644 index 00000000000..3c826827673 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/FirebasePushProviderTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.firebase + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.push.test.FakePusherSubscriber +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class FirebasePushProviderTest { + @Test + fun `test index and name`() { + val firebasePushProvider = createFirebasePushProvider() + assertThat(firebasePushProvider.name).isEqualTo(FirebaseConfig.NAME) + assertThat(firebasePushProvider.index).isEqualTo(FirebaseConfig.INDEX) + } + + @Test + fun `getDistributors return the unique distributor`() { + val firebasePushProvider = createFirebasePushProvider() + val result = firebasePushProvider.getDistributors() + assertThat(result).containsExactly(Distributor("Firebase", "Firebase")) + } + + @Test + fun `getCurrentDistributor always return the unique distributor`() = runTest { + val firebasePushProvider = createFirebasePushProvider() + val result = firebasePushProvider.getCurrentDistributor(FakeMatrixClient()) + assertThat(result).isEqualTo(Distributor("Firebase", "Firebase")) + } + + @Test + fun `isAvailable true`() { + val firebasePushProvider = createFirebasePushProvider( + isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = true) + ) + assertThat(firebasePushProvider.isAvailable()).isTrue() + } + + @Test + fun `isAvailable false`() { + val firebasePushProvider = createFirebasePushProvider( + isPlayServiceAvailable = FakeIsPlayServiceAvailable(isAvailable = false) + ) + assertThat(firebasePushProvider.isAvailable()).isFalse() + } + + @Test + fun `register ok`() = runTest { + val matrixClient = FakeMatrixClient() + val registerPusherResultLambda = lambdaRecorder> { _, _, _ -> Result.success(Unit) } + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = "aToken" + ), + pusherSubscriber = FakePusherSubscriber( + registerPusherResult = registerPusherResultLambda + ) + ) + val result = firebasePushProvider.registerWith(matrixClient, Distributor("value", "Name")) + assertThat(result).isEqualTo(Result.success(Unit)) + registerPusherResultLambda.assertions() + .isCalledOnce() + .with(value(matrixClient), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + } + + @Test + fun `register ko no token`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = null + ), + pusherSubscriber = FakePusherSubscriber( + registerPusherResult = { _, _, _ -> Result.success(Unit) } + ) + ) + val result = firebasePushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name")) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `register ko error`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = "aToken" + ), + pusherSubscriber = FakePusherSubscriber( + registerPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) } + ) + ) + val result = firebasePushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name")) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `unregister ok`() = runTest { + val matrixClient = FakeMatrixClient() + val unregisterPusherResultLambda = lambdaRecorder> { _, _, _ -> Result.success(Unit) } + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = "aToken" + ), + pusherSubscriber = FakePusherSubscriber( + unregisterPusherResult = unregisterPusherResultLambda + ) + ) + val result = firebasePushProvider.unregister(matrixClient) + assertThat(result).isEqualTo(Result.success(Unit)) + unregisterPusherResultLambda.assertions() + .isCalledOnce() + .with(value(matrixClient), value("aToken"), value(FirebaseConfig.PUSHER_HTTP_URL)) + } + + @Test + fun `unregister ko no token`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = null + ), + pusherSubscriber = FakePusherSubscriber( + unregisterPusherResult = { _, _, _ -> Result.success(Unit) } + ) + ) + val result = firebasePushProvider.unregister(FakeMatrixClient()) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `unregister ko error`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = "aToken" + ), + pusherSubscriber = FakePusherSubscriber( + unregisterPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) } + ) + ) + val result = firebasePushProvider.unregister(FakeMatrixClient()) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `getCurrentUserPushConfig no push ket`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = null + ) + ) + val result = firebasePushProvider.getCurrentUserPushConfig() + assertThat(result).isNull() + } + + @Test + fun `getCurrentUserPushConfig ok`() = runTest { + val firebasePushProvider = createFirebasePushProvider( + firebaseStore = InMemoryFirebaseStore( + token = "aToken" + ), + ) + val result = firebasePushProvider.getCurrentUserPushConfig() + assertThat(result).isEqualTo(CurrentUserPushConfig(FirebaseConfig.PUSHER_HTTP_URL, "aToken")) + } + + private fun createFirebasePushProvider( + firebaseStore: FirebaseStore = InMemoryFirebaseStore(), + pusherSubscriber: PusherSubscriber = FakePusherSubscriber(), + isPlayServiceAvailable: IsPlayServiceAvailable = FakeIsPlayServiceAvailable(false), + ): FirebasePushProvider { + return FirebasePushProvider( + firebaseStore = firebaseStore, + pusherSubscriber = pusherSubscriber, + isPlayServiceAvailable = isPlayServiceAvailable, + ) + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt new file mode 100644 index 00000000000..9dfb4539199 --- /dev/null +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/VectorFirebaseMessagingServiceTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.pushproviders.firebase + +import android.os.Bundle +import com.google.firebase.messaging.RemoteMessage +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.push.test.test.FakePushHandler +import io.element.android.libraries.pushproviders.api.PushData +import io.element.android.libraries.pushproviders.api.PushHandler +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class VectorFirebaseMessagingServiceTest { + @Test + fun `test receiving invalid data`() = runTest { + val lambda = lambdaRecorder(ensureNeverCalled = true) { } + val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( + pushHandler = FakePushHandler(handleResult = lambda) + ) + vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle())) + } + + @Test + fun `test receiving valid data`() = runTest { + val lambda = lambdaRecorder { } + val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( + pushHandler = FakePushHandler(handleResult = lambda) + ) + vectorFirebaseMessagingService.onMessageReceived( + message = RemoteMessage( + Bundle().apply { + putString("event_id", AN_EVENT_ID.value) + putString("room_id", A_ROOM_ID.value) + putString("cs", A_SECRET) + }, + ) + ) + advanceUntilIdle() + lambda.assertions() + .isCalledOnce() + .with(value(PushData(AN_EVENT_ID, A_ROOM_ID, null, A_SECRET))) + } + + @Test + fun `test new token is forwarded to the handler`() = runTest { + val lambda = lambdaRecorder { } + val vectorFirebaseMessagingService = createVectorFirebaseMessagingService( + firebaseNewTokenHandler = FakeFirebaseNewTokenHandler(handleResult = lambda) + ) + vectorFirebaseMessagingService.onNewToken("aToken") + advanceUntilIdle() + lambda.assertions() + .isCalledOnce() + .with(value("aToken")) + } + + private fun TestScope.createVectorFirebaseMessagingService( + firebaseNewTokenHandler: FirebaseNewTokenHandler = FakeFirebaseNewTokenHandler(), + pushHandler: PushHandler = FakePushHandler(), + ): VectorFirebaseMessagingService { + return VectorFirebaseMessagingService().apply { + this.firebaseNewTokenHandler = firebaseNewTokenHandler + this.pushParser = FirebasePushParser() + this.pushHandler = pushHandler + this.coroutineScope = this@createVectorFirebaseMessagingService + } + } +} diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt index 6f1a3da7cb5..fae5ba9f121 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseAvailabilityTestTest.kt @@ -18,8 +18,10 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.pushproviders.firebase.IsPlayServiceAvailable +import io.element.android.libraries.pushproviders.firebase.FakeIsPlayServiceAvailable +import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -29,11 +31,7 @@ class FirebaseAvailabilityTestTest { @Test fun `test FirebaseAvailabilityTest success`() = runTest { val sut = FirebaseAvailabilityTest( - isPlayServiceAvailable = object : IsPlayServiceAvailable { - override fun isAvailable(): Boolean { - return true - } - }, + isPlayServiceAvailable = FakeIsPlayServiceAvailable(true), stringProvider = FakeStringProvider(), ) launch { @@ -50,11 +48,7 @@ class FirebaseAvailabilityTestTest { @Test fun `test FirebaseAvailabilityTest failure`() = runTest { val sut = FirebaseAvailabilityTest( - isPlayServiceAvailable = object : IsPlayServiceAvailable { - override fun isAvailable(): Boolean { - return false - } - }, + isPlayServiceAvailable = FakeIsPlayServiceAvailable(false), stringProvider = FakeStringProvider(), ) launch { @@ -67,4 +61,14 @@ class FirebaseAvailabilityTestTest { assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) } } + + @Test + fun `test FirebaseAvailabilityTest isRelevant`() { + val sut = FirebaseAvailabilityTest( + isPlayServiceAvailable = FakeIsPlayServiceAvailable(false), + stringProvider = FakeStringProvider(), + ) + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "unknown"))).isFalse() + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = FirebaseConfig.NAME))).isTrue() + } } diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt index 2d8de62ad92..245a6095d41 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/pushproviders/firebase/troubleshoot/FirebaseTokenTestTest.kt @@ -19,8 +19,10 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter +import io.element.android.libraries.pushproviders.firebase.FirebaseConfig import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -75,6 +77,17 @@ class FirebaseTokenTestTest { } } + @Test + fun `test FirebaseTokenTest isRelevant`() { + val sut = FirebaseTokenTest( + firebaseStore = InMemoryFirebaseStore(null), + firebaseTroubleshooter = FakeFirebaseTroubleshooter(), + stringProvider = FakeStringProvider(), + ) + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "unknown"))).isFalse() + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = FirebaseConfig.NAME))).isTrue() + } + companion object { private const val FAKE_TOKEN = "abcdefghijk" } diff --git a/libraries/pushproviders/test/build.gradle.kts b/libraries/pushproviders/test/build.gradle.kts index ddb68ed43fb..9a0d2c139c5 100644 --- a/libraries/pushproviders/test/build.gradle.kts +++ b/libraries/pushproviders/test/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.pushproviders.api) + implementation(projects.tests.testutils) } diff --git a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt index 44aa1ca18f1..7b37d0d2966 100644 --- a/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt +++ b/libraries/pushproviders/test/src/main/kotlin/io/element/android/libraries/pushproviders/test/FakePushProvider.kt @@ -20,19 +20,23 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.PushProvider +import io.element.android.tests.testutils.lambda.lambdaError class FakePushProvider( override val index: Int = 0, override val name: String = "aFakePushProvider", private val isAvailable: Boolean = true, private val distributors: List = listOf(Distributor("aDistributorValue", "aDistributorName")), + private val currentUserPushConfig: CurrentUserPushConfig? = null, + private val registerWithResult: (MatrixClient, Distributor) -> Result = { _, _ -> lambdaError() }, + private val unregisterWithResult: (MatrixClient) -> Result = { lambdaError() }, ) : PushProvider { override fun isAvailable(): Boolean = isAvailable override fun getDistributors(): List = distributors override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result { - return Result.success(Unit) + return registerWithResult(matrixClient, distributor) } override suspend fun getCurrentDistributor(matrixClient: MatrixClient): Distributor? { @@ -40,10 +44,10 @@ class FakePushProvider( } override suspend fun unregister(matrixClient: MatrixClient): Result { - return Result.success(Unit) + return unregisterWithResult(matrixClient) } override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { - return null + return currentUserPushConfig } } diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index d5dcc9727d2..d1c8cf5ee1f 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -55,9 +55,13 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.test.junit) + testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.push.test) + testImplementation(projects.libraries.pushstore.test) testImplementation(projects.tests.testutils) testImplementation(projects.services.toolbox.test) + testImplementation(projects.services.appnavstate.test) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt index 24f93b88dcf..6670f18ea2f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -17,43 +17,41 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject import kotlin.time.Duration.Companion.seconds -class RegisterUnifiedPushUseCase @Inject constructor( +interface RegisterUnifiedPushUseCase { + suspend fun execute(distributor: Distributor, clientSecret: String): Result +} + +@ContributesBinding(AppScope::class) +class DefaultRegisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, private val endpointRegistrationHandler: EndpointRegistrationHandler, - private val coroutineScope: CoroutineScope, -) { - suspend fun execute(distributor: Distributor, clientSecret: String): Result { +) : RegisterUnifiedPushUseCase { + override suspend fun execute(distributor: Distributor, clientSecret: String): Result { UnifiedPush.saveDistributor(context, distributor.value) - val completable = CompletableDeferred>() - val job = coroutineScope.launch { - val result = endpointRegistrationHandler.state - .filter { it.clientSecret == clientSecret } - .first() - .result - completable.complete(result) - } // This will trigger the callback // VectorUnifiedPushMessagingReceiver.onNewEndpoint UnifiedPush.registerApp(context = context, instance = clientSecret) // Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed - return withTimeout(30.seconds) { - completable.await() - } - .onFailure { - job.cancel() + return runCatching { + withTimeout(30.seconds) { + val result = endpointRegistrationHandler.state + .filter { it.clientSecret == clientSecret } + .first() + .result + result.getOrThrow() } + } } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt new file mode 100644 index 00000000000..84a923df447 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushApiFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi +import javax.inject.Inject + +interface UnifiedPushApiFactory { + fun create(baseUrl: String): UnifiedPushApi +} + +@ContributesBinding(AppScope::class) +class DefaultUnifiedPushApiFactory @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) : UnifiedPushApiFactory { + override fun create(baseUrl: String): UnifiedPushApi { + return retrofitFactory.create(baseUrl) + .create(UnifiedPushApi::class.java) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt index 54b80a51103..c39c7ec0668 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushGatewayResolver.kt @@ -16,29 +16,33 @@ package io.element.android.libraries.pushproviders.unifiedpush +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.network.RetrofitFactory -import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi +import io.element.android.libraries.di.AppScope import kotlinx.coroutines.withContext import timber.log.Timber import java.net.URL import javax.inject.Inject -class UnifiedPushGatewayResolver @Inject constructor( - private val retrofitFactory: RetrofitFactory, +interface UnifiedPushGatewayResolver { + suspend fun getGateway(endpoint: String): String +} + +@ContributesBinding(AppScope::class) +class DefaultUnifiedPushGatewayResolver @Inject constructor( + private val unifiedPushApiFactory: UnifiedPushApiFactory, private val coroutineDispatchers: CoroutineDispatchers, -) { - suspend fun getGateway(endpoint: String): String? { +) : UnifiedPushGatewayResolver { + override suspend fun getGateway(endpoint: String): String { val gateway = UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL - val url = URL(endpoint) - val port = if (url.port != -1) ":${url.port}" else "" - val customBase = "${url.protocol}://${url.host}$port" - val customUrl = "$customBase/_matrix/push/v1/notify" - Timber.i("Testing $customUrl") try { + val url = URL(endpoint) + val port = if (url.port != -1) ":${url.port}" else "" + val customBase = "${url.protocol}://${url.host}$port" + val customUrl = "$customBase/_matrix/push/v1/notify" + Timber.i("Testing $customUrl") return withContext(coroutineDispatchers.io) { - val api = retrofitFactory.create(customBase) - .create(UnifiedPushApi::class.java) + val api = unifiedPushApiFactory.create(customBase) try { val discoveryResponse = api.discover() if (discoveryResponse.unifiedpush.gateway == "matrix") { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt index 3ae733d2615..f839e6a03ee 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.pushproviders.unifiedpush +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.extensions.flatMap import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory @@ -25,18 +27,23 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag) +private val loggerTag = LoggerTag("DefaultUnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag) /** * Handle new endpoint received from UnifiedPush. Will update the session matching the client secret. */ -class UnifiedPushNewGatewayHandler @Inject constructor( +interface UnifiedPushNewGatewayHandler { + suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result +} + +@ContributesBinding(AppScope::class) +class DefaultUnifiedPushNewGatewayHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val matrixAuthenticationService: MatrixAuthenticationService, -) { - suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result { +) : UnifiedPushNewGatewayHandler { + override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result { // Register the pusher for the session with this client secret, if is it using UnifiedPush. val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure( IllegalStateException("Unable to retrieve session") diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt index f68cd8542b9..46c7c0f9bb8 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParser.kt @@ -18,7 +18,6 @@ package io.element.android.libraries.pushproviders.unifiedpush import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.pushproviders.api.PushData -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import javax.inject.Inject diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt index 7b6ee5ef37d..dc1bd86d9e1 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushStore.kt @@ -19,32 +19,44 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.matrix.api.core.UserId import javax.inject.Inject -class UnifiedPushStore @Inject constructor( +interface UnifiedPushStore { + fun getEndpoint(clientSecret: String): String? + fun storeUpEndpoint(clientSecret: String, endpoint: String?) + fun getPushGateway(clientSecret: String): String? + fun storePushGateway(clientSecret: String, gateway: String?) + fun getDistributorValue(userId: UserId): String? + fun setDistributorValue(userId: UserId, value: String) +} + +@ContributesBinding(AppScope::class) +class DefaultUnifiedPushStore @Inject constructor( @ApplicationContext val context: Context, @DefaultPreferences private val defaultPrefs: SharedPreferences, -) { +) : UnifiedPushStore { /** * Retrieves the UnifiedPush Endpoint. * * @param clientSecret the client secret, to identify the session * @return the UnifiedPush Endpoint or null if not received */ - fun getEndpoint(clientSecret: String): String? { + override fun getEndpoint(clientSecret: String): String? { return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null) } /** * Store UnifiedPush Endpoint to the SharedPrefs. * - * @param endpoint the endpoint to store * @param clientSecret the client secret, to identify the session + * @param endpoint the endpoint to store */ - fun storeUpEndpoint(endpoint: String?, clientSecret: String) { + override fun storeUpEndpoint(clientSecret: String, endpoint: String?) { defaultPrefs.edit { putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint) } @@ -56,27 +68,27 @@ class UnifiedPushStore @Inject constructor( * @param clientSecret the client secret, to identify the session * @return the Push Gateway or null if not defined */ - fun getPushGateway(clientSecret: String): String? { + override fun getPushGateway(clientSecret: String): String? { return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null) } /** * Store Push Gateway to the SharedPrefs. * - * @param gateway the push gateway to store * @param clientSecret the client secret, to identify the session + * @param gateway the push gateway to store */ - fun storePushGateway(gateway: String?, clientSecret: String) { + override fun storePushGateway(clientSecret: String, gateway: String?) { defaultPrefs.edit { putString(PREFS_PUSH_GATEWAY + clientSecret, gateway) } } - fun getDistributorValue(userId: UserId): String? { + override fun getDistributorValue(userId: UserId): String? { return defaultPrefs.getString(PREFS_DISTRIBUTOR + userId, null) } - fun setDistributorValue(userId: UserId, value: String) { + override fun setDistributorValue(userId: UserId, value: String) { defaultPrefs.edit { putString(PREFS_DISTRIBUTOR + userId, value) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt index b2dd33c252f..65d924740de 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -17,18 +17,25 @@ package io.element.android.libraries.pushproviders.unifiedpush import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.pushproviders.api.PusherSubscriber import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject -class UnregisterUnifiedPushUseCase @Inject constructor( +interface UnregisterUnifiedPushUseCase { + suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result +} + +@ContributesBinding(AppScope::class) +class DefaultUnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, private val unifiedPushStore: UnifiedPushStore, private val pusherSubscriber: PusherSubscriber, -) { - suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result { +) : UnregisterUnifiedPushUseCase { + override suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result { val endpoint = unifiedPushStore.getEndpoint(clientSecret) val gateway = unifiedPushStore.getPushGateway(clientSecret) if (endpoint == null || gateway == null) { @@ -36,8 +43,8 @@ class UnregisterUnifiedPushUseCase @Inject constructor( } return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway) .onSuccess { - unifiedPushStore.storeUpEndpoint(null, clientSecret) - unifiedPushStore.storePushGateway(null, clientSecret) + unifiedPushStore.storeUpEndpoint(clientSecret, null) + unifiedPushStore.storePushGateway(clientSecret, null) UnifiedPush.unregisterApp(context) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index c47aabae37e..a52b1b0e6e1 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -24,7 +24,6 @@ import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber @@ -40,8 +39,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler - - private val coroutineScope = CoroutineScope(SupervisorJob()) + @Inject lateinit var coroutineScope: CoroutineScope override fun onReceive(context: Context, intent: Intent) { context.applicationContext.bindings().inject(this) @@ -75,30 +73,20 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { Timber.tag(loggerTag.value).i("onNewEndpoint: $endpoint") coroutineScope.launch { val gateway = unifiedPushGatewayResolver.getGateway(endpoint) - unifiedPushStore.storePushGateway(gateway, instance) - if (gateway == null) { - Timber.tag(loggerTag.value).w("No gateway found for endpoint $endpoint") - endpointRegistrationHandler.registrationDone( - RegistrationResult( - clientSecret = instance, - result = Result.failure(IllegalStateException("No gateway found for endpoint $endpoint")), - ) - ) - } else { - val result = newGatewayHandler.handle(endpoint, gateway, instance) - .onFailure { - Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway") - } - .onSuccess { - unifiedPushStore.storeUpEndpoint(endpoint, instance) - } - endpointRegistrationHandler.registrationDone( - RegistrationResult( - clientSecret = instance, - result = result, - ) + unifiedPushStore.storePushGateway(instance, gateway) + val result = newGatewayHandler.handle(endpoint, gateway, instance) + .onFailure { + Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway") + } + .onSuccess { + unifiedPushStore.storeUpEndpoint(instance, endpoint) + } + endpointRegistrationHandler.registrationDone( + RegistrationResult( + clientSecret = instance, + result = result, ) - } + ) } guardServiceStarter.stop() } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt new file mode 100644 index 00000000000..cda9516b659 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultRegisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler +import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultRegisterUnifiedPushUseCaseTest { + @Test + fun `test registration successful`() = runTest { + val endpointRegistrationHandler = EndpointRegistrationHandler() + val useCase = createDefaultRegisterUnifiedPushUseCase( + endpointRegistrationHandler = endpointRegistrationHandler + ) + val aDistributor = Distributor("aValue", "aName") + launch { + delay(100) + endpointRegistrationHandler.registrationDone(RegistrationResult(A_SECRET, Result.success(Unit))) + } + val result = useCase.execute(aDistributor, A_SECRET) + assertThat(result.isSuccess).isTrue() + } + + @Test + fun `test registration error`() = runTest { + val endpointRegistrationHandler = EndpointRegistrationHandler() + val useCase = createDefaultRegisterUnifiedPushUseCase( + endpointRegistrationHandler = endpointRegistrationHandler + ) + val aDistributor = Distributor("aValue", "aName") + launch { + delay(100) + endpointRegistrationHandler.registrationDone(RegistrationResult(A_SECRET, Result.failure(AN_EXCEPTION))) + } + val result = useCase.execute(aDistributor, A_SECRET) + assertThat(result.isSuccess).isFalse() + } + + @Test + fun `test registration timeout`() = runTest { + val endpointRegistrationHandler = EndpointRegistrationHandler() + val useCase = createDefaultRegisterUnifiedPushUseCase( + endpointRegistrationHandler = endpointRegistrationHandler + ) + val aDistributor = Distributor("aValue", "aName") + val result = useCase.execute(aDistributor, A_SECRET) + assertThat(result.isSuccess).isFalse() + } + + private fun TestScope.createDefaultRegisterUnifiedPushUseCase( + endpointRegistrationHandler: EndpointRegistrationHandler + ): DefaultRegisterUnifiedPushUseCase { + val context = InstrumentationRegistry.getInstrumentation().context + return DefaultRegisterUnifiedPushUseCase( + context = context, + endpointRegistrationHandler = endpointRegistrationHandler, + ) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt new file mode 100644 index 00000000000..e180e36c389 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushGatewayResolverTest.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse +import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryUnifiedPush +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultUnifiedPushGatewayResolverTest { + private val matrixDiscoveryResponse = { + DiscoveryResponse( + unifiedpush = DiscoveryUnifiedPush( + gateway = "matrix" + ) + ) + } + + private val invalidDiscoveryResponse = { + DiscoveryResponse( + unifiedpush = DiscoveryUnifiedPush( + gateway = "" + ) + ) + } + + @Test + fun `when a custom url provide a correct matrix gateway, the custom url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = matrixDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("https://custom.url") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("https://custom.url") + assertThat(result).isEqualTo("https://custom.url/_matrix/push/v1/notify") + } + + @Test + fun `when a custom url with port provides a correct matrix gateway, the custom url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = matrixDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("https://custom.url:123") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("https://custom.url:123") + assertThat(result).isEqualTo("https://custom.url:123/_matrix/push/v1/notify") + } + + @Test + fun `when a custom url with port and path provides a correct matrix gateway, the custom url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = matrixDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("https://custom.url:123/some/path") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("https://custom.url:123") + assertThat(result).isEqualTo("https://custom.url:123/_matrix/push/v1/notify") + } + + @Test + fun `when a custom url with http scheme provides a correct matrix gateway, the custom url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = matrixDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("http://custom.url:123/some/path") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url:123") + assertThat(result).isEqualTo("http://custom.url:123/_matrix/push/v1/notify") + } + + @Test + fun `when a custom url is not reachable, the default url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = { throw AN_EXCEPTION } + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("http://custom.url") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url") + assertThat(result).isEqualTo(UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL) + } + + @Test + fun `when a custom url is invalid, the default url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = matrixDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("invalid") + assertThat(unifiedPushApiFactory.baseUrlParameter).isNull() + assertThat(result).isEqualTo(UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL) + } + + @Test + fun `when a custom url provides a invalid matrix gateway, the default url is returned`() = runTest { + val unifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = invalidDiscoveryResponse + ) + val sut = createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory + ) + val result = sut.getGateway("https://custom.url") + assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("https://custom.url") + assertThat(result).isEqualTo(UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL) + } + + private fun TestScope.createDefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory: UnifiedPushApiFactory = FakeUnifiedPushApiFactory( + discoveryResponse = { DiscoveryResponse() } + ) + ) = DefaultUnifiedPushGatewayResolver( + unifiedPushApiFactory = unifiedPushApiFactory, + coroutineDispatchers = testCoroutineDispatchers() + ) +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushNewGatewayHandlerTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushNewGatewayHandlerTest.kt new file mode 100644 index 00000000000..3f0b7409cca --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnifiedPushNewGatewayHandlerTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.libraries.push.test.FakePusherSubscriber +import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore +import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory +import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultUnifiedPushNewGatewayHandlerTest { + @Test + fun `error when fail to retrieve the session`() = runTest { + val defaultUnifiedPushNewGatewayHandler = createDefaultUnifiedPushNewGatewayHandler( + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { null } + ) + ) + val result = defaultUnifiedPushNewGatewayHandler.handle( + endpoint = "aEndpoint", + pushGateway = "aPushGateway", + clientSecret = A_SECRET, + ) + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + assertThat(result.exceptionOrNull()?.message).isEqualTo("Unable to retrieve session") + } + + @Test + fun `error when the session is not using UnifiedPush`() = runTest { + val defaultUnifiedPushNewGatewayHandler = createDefaultUnifiedPushNewGatewayHandler( + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { FakeUserPushStore(pushProviderName = "other") } + ) + ) + val result = defaultUnifiedPushNewGatewayHandler.handle( + endpoint = "aEndpoint", + pushGateway = "aPushGateway", + clientSecret = A_SECRET, + ) + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + assertThat(result.exceptionOrNull()?.message).isEqualTo("This session is not using UnifiedPush pusher") + } + + @Test + fun `error when the registration fails`() = runTest { + val aMatrixClient = FakeMatrixClient() + val defaultUnifiedPushNewGatewayHandler = createDefaultUnifiedPushNewGatewayHandler( + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { FakeUserPushStore(pushProviderName = UnifiedPushConfig.NAME) } + ), + pusherSubscriber = FakePusherSubscriber( + registerPusherResult = { _, _, _ -> Result.failure(IllegalStateException("an error")) } + ), + matrixAuthenticationService = FakeAuthenticationService(matrixClientResult = { Result.success(aMatrixClient) }), + ) + val result = defaultUnifiedPushNewGatewayHandler.handle( + endpoint = "aEndpoint", + pushGateway = "aPushGateway", + clientSecret = A_SECRET, + ) + assertThat(result.isFailure).isTrue() + assertThat(result.exceptionOrNull()).isInstanceOf(IllegalStateException::class.java) + assertThat(result.exceptionOrNull()?.message).isEqualTo("an error") + } + + @Test + fun `happy path`() = runTest { + val aMatrixClient = FakeMatrixClient() + val lambda = lambdaRecorder { _: MatrixClient, _: String, _: String -> + Result.success(Unit) + } + val defaultUnifiedPushNewGatewayHandler = createDefaultUnifiedPushNewGatewayHandler( + pushClientSecret = FakePushClientSecret( + getUserIdFromSecretResult = { A_USER_ID } + ), + userPushStoreFactory = FakeUserPushStoreFactory( + userPushStore = { FakeUserPushStore(pushProviderName = UnifiedPushConfig.NAME) } + ), + pusherSubscriber = FakePusherSubscriber( + registerPusherResult = lambda + ), + matrixAuthenticationService = FakeAuthenticationService(matrixClientResult = { Result.success(aMatrixClient) }), + ) + val result = defaultUnifiedPushNewGatewayHandler.handle( + endpoint = "aEndpoint", + pushGateway = "aPushGateway", + clientSecret = A_SECRET, + ) + assertThat(result).isEqualTo(Result.success(Unit)) + lambda.assertions() + .isCalledOnce() + .with(value(aMatrixClient), value("aEndpoint"), value("aPushGateway")) + } + + private fun createDefaultUnifiedPushNewGatewayHandler( + pusherSubscriber: PusherSubscriber = FakePusherSubscriber(), + userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), + pushClientSecret: PushClientSecret = FakePushClientSecret(), + matrixAuthenticationService: MatrixAuthenticationService = FakeAuthenticationService() + ): DefaultUnifiedPushNewGatewayHandler { + return DefaultUnifiedPushNewGatewayHandler( + pusherSubscriber = pusherSubscriber, + userPushStoreFactory = userPushStoreFactory, + pushClientSecret = pushClientSecret, + matrixAuthenticationService = matrixAuthenticationService + ) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt new file mode 100644 index 00000000000..1d04a07651f --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/DefaultUnregisterUnifiedPushUseCaseTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.push.test.FakePusherSubscriber +import io.element.android.libraries.pushproviders.api.PusherSubscriber +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DefaultUnregisterUnifiedPushUseCaseTest { + @Test + fun `test un registration successful`() = runTest { + val lambda = lambdaRecorder { _: MatrixClient, _: String, _: String -> Result.success(Unit) } + val storeUpEndpointResult = lambdaRecorder { _: String, _: String? -> } + val storePushGatewayResult = lambdaRecorder { _: String, _: String? -> } + val matrixClient = FakeMatrixClient() + val useCase = createDefaultUnregisterUnifiedPushUseCase( + unifiedPushStore = FakeUnifiedPushStore( + getEndpointResult = { "aEndpoint" }, + getPushGatewayResult = { "aGateway" }, + storeUpEndpointResult = storeUpEndpointResult, + storePushGatewayResult = storePushGatewayResult, + ), + pusherSubscriber = FakePusherSubscriber( + unregisterPusherResult = lambda + ) + ) + val result = useCase.execute(matrixClient, A_SECRET) + assertThat(result.isSuccess).isTrue() + lambda.assertions() + .isCalledOnce() + .with(value(matrixClient), value("aEndpoint"), value("aGateway")) + storeUpEndpointResult.assertions() + .isCalledOnce() + .with(value(A_SECRET), value(null)) + storePushGatewayResult.assertions() + .isCalledOnce() + .with(value(A_SECRET), value(null)) + } + + @Test + fun `test un registration error - no endpoint`() = runTest { + val matrixClient = FakeMatrixClient() + val useCase = createDefaultUnregisterUnifiedPushUseCase( + unifiedPushStore = FakeUnifiedPushStore( + getEndpointResult = { null }, + getPushGatewayResult = { "aGateway" }, + ), + ) + val result = useCase.execute(matrixClient, A_SECRET) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `test un registration error - no gateway`() = runTest { + val matrixClient = FakeMatrixClient() + val useCase = createDefaultUnregisterUnifiedPushUseCase( + unifiedPushStore = FakeUnifiedPushStore( + getEndpointResult = { "aEndpoint" }, + getPushGatewayResult = { null }, + ), + ) + val result = useCase.execute(matrixClient, A_SECRET) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `test un registration error`() = runTest { + val matrixClient = FakeMatrixClient() + val useCase = createDefaultUnregisterUnifiedPushUseCase( + unifiedPushStore = FakeUnifiedPushStore( + getEndpointResult = { "aEndpoint" }, + getPushGatewayResult = { "aGateway" }, + ), + pusherSubscriber = FakePusherSubscriber( + unregisterPusherResult = { _, _, _ -> Result.failure(AN_EXCEPTION) } + ) + ) + val result = useCase.execute(matrixClient, A_SECRET) + assertThat(result.isFailure).isTrue() + } + + private fun createDefaultUnregisterUnifiedPushUseCase( + unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(), + pusherSubscriber: PusherSubscriber = FakePusherSubscriber() + ): DefaultUnregisterUnifiedPushUseCase { + val context = InstrumentationRegistry.getInstrumentation().context + return DefaultUnregisterUnifiedPushUseCase( + context = context, + unifiedPushStore = unifiedPushStore, + pusherSubscriber = pusherSubscriber + ) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeRegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeRegisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..1800903dead --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeRegisterUnifiedPushUseCase.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeRegisterUnifiedPushUseCase( + private val result: (Distributor, String) -> Result = { _, _ -> lambdaError() } +) : RegisterUnifiedPushUseCase { + override suspend fun execute(distributor: Distributor, clientSecret: String): Result { + return result(distributor, clientSecret) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushApiFactory.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushApiFactory.kt new file mode 100644 index 00000000000..e0d7808505c --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushApiFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse +import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi + +class FakeUnifiedPushApiFactory( + private val discoveryResponse: () -> DiscoveryResponse +) : UnifiedPushApiFactory { + var baseUrlParameter: String? = null + private set + + override fun create(baseUrl: String): UnifiedPushApi { + baseUrlParameter = baseUrl + return FakeUnifiedPushApi(discoveryResponse) + } +} + +class FakeUnifiedPushApi( + private val discoveryResponse: () -> DiscoveryResponse +) : UnifiedPushApi { + override suspend fun discover(): DiscoveryResponse { + return discoveryResponse() + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushGatewayResolver.kt new file mode 100644 index 00000000000..0bc52fbae8e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushGatewayResolver.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeUnifiedPushGatewayResolver( + private val getGatewayResult: (String) -> String = { lambdaError() }, +) : UnifiedPushGatewayResolver { + override suspend fun getGateway(endpoint: String): String { + return getGatewayResult(endpoint) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushNewGatewayHandler.kt new file mode 100644 index 00000000000..b8d70baadab --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushNewGatewayHandler.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeUnifiedPushNewGatewayHandler( + private val handleResult: suspend (String, String, String) -> Result = { _, _, _ -> lambdaError() }, +) : UnifiedPushNewGatewayHandler { + override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result { + return handleResult(endpoint, pushGateway, clientSecret) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushStore.kt new file mode 100644 index 00000000000..aa381d95359 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnifiedPushStore.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeUnifiedPushStore( + private val getEndpointResult: (String) -> String? = { lambdaError() }, + private val storeUpEndpointResult: (String, String?) -> Unit = { _, _ -> lambdaError() }, + private val getPushGatewayResult: (String) -> String? = { lambdaError() }, + private val storePushGatewayResult: (String, String?) -> Unit = { _, _ -> lambdaError() }, + private val getDistributorValueResult: (UserId) -> String? = { lambdaError() }, + private val setDistributorValueResult: (UserId, String) -> Unit = { _, _ -> lambdaError() }, +) : UnifiedPushStore { + override fun getEndpoint(clientSecret: String): String? { + return getEndpointResult(clientSecret) + } + + override fun storeUpEndpoint(clientSecret: String, endpoint: String?) { + storeUpEndpointResult(clientSecret, endpoint) + } + + override fun getPushGateway(clientSecret: String): String? { + return getPushGatewayResult(clientSecret) + } + + override fun storePushGateway(clientSecret: String, gateway: String?) { + storePushGatewayResult(clientSecret, gateway) + } + + override fun getDistributorValue(userId: UserId): String? { + return getDistributorValueResult(userId) + } + + override fun setDistributorValue(userId: UserId, value: String) { + setDistributorValueResult(userId, value) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnregisterUnifiedPushUseCase.kt new file mode 100644 index 00000000000..9f3293420ab --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/FakeUnregisterUnifiedPushUseCase.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeUnregisterUnifiedPushUseCase( + private val result: (MatrixClient, String) -> Result = { _, _ -> lambdaError() } +) : UnregisterUnifiedPushUseCase { + override suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result { + return result(matrixClient, clientSecret) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParserTest.kt index bbccc925815..da710037c43 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushParserTest.kt @@ -82,9 +82,8 @@ class UnifiedPushParserTest { } companion object { - private val UNIFIED_PUSH_DATA = + val UNIFIED_PUSH_DATA = "{\"notification\":{\"event_id\":\"$AN_EVENT_ID\",\"room_id\":\"$A_ROOM_ID\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" - // TODO Check client secret format? } } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProviderTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProviderTest.kt new file mode 100644 index 00000000000..826f08a1b0c --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/UnifiedPushProviderTest.kt @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushproviders.unifiedpush + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig +import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.unifiedpush.troubleshoot.FakeUnifiedPushDistributorProvider +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class UnifiedPushProviderTest { + @Test + fun `test index and name`() { + val unifiedPushProvider = createUnifiedPushProvider() + assertThat(unifiedPushProvider.name).isEqualTo(UnifiedPushConfig.NAME) + assertThat(unifiedPushProvider.index).isEqualTo(UnifiedPushConfig.INDEX) + } + + @Test + fun `getDistributors return the available distributors`() { + val unifiedPushProvider = createUnifiedPushProvider( + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = listOf( + Distributor("value", "Name"), + ) + ) + ) + val result = unifiedPushProvider.getDistributors() + assertThat(result).containsExactly(Distributor("value", "Name")) + assertThat(unifiedPushProvider.isAvailable()).isTrue() + } + + @Test + fun `getDistributors return empty`() { + val unifiedPushProvider = createUnifiedPushProvider( + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = emptyList() + ) + ) + val result = unifiedPushProvider.getDistributors() + assertThat(result).isEmpty() + assertThat(unifiedPushProvider.isAvailable()).isFalse() + } + + @Test + fun `register ok`() = runTest { + val getSecretForUserResultLambda = lambdaRecorder { A_SECRET } + val executeLambda = lambdaRecorder> { _, _ -> Result.success(Unit) } + val setDistributorValueResultLambda = lambdaRecorder { _, _ -> } + val unifiedPushProvider = createUnifiedPushProvider( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = getSecretForUserResultLambda, + ), + registerUnifiedPushUseCase = FakeRegisterUnifiedPushUseCase( + result = executeLambda, + ), + unifiedPushStore = FakeUnifiedPushStore( + setDistributorValueResult = setDistributorValueResultLambda, + ), + ) + val result = unifiedPushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name")) + assertThat(result).isEqualTo(Result.success(Unit)) + getSecretForUserResultLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + executeLambda.assertions() + .isCalledOnce() + .with(value(Distributor("value", "Name")), value(A_SECRET)) + setDistributorValueResultLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID), value("value")) + } + + @Test + fun `register ko`() = runTest { + val getSecretForUserResultLambda = lambdaRecorder { A_SECRET } + val executeLambda = lambdaRecorder> { _, _ -> Result.failure(AN_EXCEPTION) } + val setDistributorValueResultLambda = lambdaRecorder(ensureNeverCalled = true) { _, _ -> } + val unifiedPushProvider = createUnifiedPushProvider( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = getSecretForUserResultLambda, + ), + registerUnifiedPushUseCase = FakeRegisterUnifiedPushUseCase( + result = executeLambda, + ), + unifiedPushStore = FakeUnifiedPushStore( + setDistributorValueResult = setDistributorValueResultLambda, + ), + ) + val result = unifiedPushProvider.registerWith(FakeMatrixClient(), Distributor("value", "Name")) + assertThat(result).isEqualTo(Result.failure(AN_EXCEPTION)) + getSecretForUserResultLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + executeLambda.assertions() + .isCalledOnce() + .with(value(Distributor("value", "Name")), value(A_SECRET)) + } + + @Test + fun `unregister ok`() = runTest { + val matrixClient = FakeMatrixClient() + val getSecretForUserResultLambda = lambdaRecorder { A_SECRET } + val executeLambda = lambdaRecorder> { _, _ -> Result.success(Unit) } + val unifiedPushProvider = createUnifiedPushProvider( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = getSecretForUserResultLambda, + ), + unRegisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase( + result = executeLambda, + ), + ) + val result = unifiedPushProvider.unregister(matrixClient) + assertThat(result).isEqualTo(Result.success(Unit)) + getSecretForUserResultLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + executeLambda.assertions() + .isCalledOnce() + .with(value(matrixClient), value(A_SECRET)) + } + + @Test + fun `unregister ko`() = runTest { + val matrixClient = FakeMatrixClient() + val getSecretForUserResultLambda = lambdaRecorder { A_SECRET } + val executeLambda = lambdaRecorder> { _, _ -> Result.failure(AN_EXCEPTION) } + val unifiedPushProvider = createUnifiedPushProvider( + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = getSecretForUserResultLambda, + ), + unRegisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase( + result = executeLambda, + ), + ) + val result = unifiedPushProvider.unregister(matrixClient) + assertThat(result).isEqualTo(Result.failure(AN_EXCEPTION)) + getSecretForUserResultLambda.assertions() + .isCalledOnce() + .with(value(A_SESSION_ID)) + executeLambda.assertions() + .isCalledOnce() + .with(value(matrixClient), value(A_SECRET)) + } + + @Test + fun `getCurrentDistributor ok`() = runTest { + val distributor = Distributor("value", "Name") + val matrixClient = FakeMatrixClient() + val unifiedPushProvider = createUnifiedPushProvider( + unifiedPushStore = FakeUnifiedPushStore( + getDistributorValueResult = { distributor.value } + ), + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = listOf( + Distributor("value2", "Name2"), + distributor, + ) + ) + ) + val result = unifiedPushProvider.getCurrentDistributor(matrixClient) + assertThat(result).isEqualTo(distributor) + } + + @Test + fun `getCurrentDistributor not know`() = runTest { + val distributor = Distributor("value", "Name") + val matrixClient = FakeMatrixClient() + val unifiedPushProvider = createUnifiedPushProvider( + unifiedPushStore = FakeUnifiedPushStore( + getDistributorValueResult = { "unknown" } + ), + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = listOf( + distributor, + ) + ) + ) + val result = unifiedPushProvider.getCurrentDistributor(matrixClient) + assertThat(result).isNull() + } + + @Test + fun `getCurrentDistributor not found`() = runTest { + val distributor = Distributor("value", "Name") + val matrixClient = FakeMatrixClient() + val unifiedPushProvider = createUnifiedPushProvider( + unifiedPushStore = FakeUnifiedPushStore( + getDistributorValueResult = { distributor.value } + ), + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider( + getDistributorsResult = emptyList() + ) + ) + val result = unifiedPushProvider.getCurrentDistributor(matrixClient) + assertThat(result).isNull() + } + + @Test + fun `getCurrentUserPushConfig no session`() = runTest { + val unifiedPushProvider = createUnifiedPushProvider() + val result = unifiedPushProvider.getCurrentUserPushConfig() + assertThat(result).isNull() + } + + @Test + fun `getCurrentUserPushConfig no push gateway`() = runTest { + val unifiedPushProvider = createUnifiedPushProvider( + appNavigationStateService = FakeAppNavigationStateService( + appNavigationState = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Session(owner = "owner", sessionId = A_SESSION_ID), + isInForeground = true + ) + ) + ), + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = { A_SECRET } + ), + unifiedPushStore = FakeUnifiedPushStore( + getPushGatewayResult = { null } + ), + ) + val result = unifiedPushProvider.getCurrentUserPushConfig() + assertThat(result).isNull() + } + + @Test + fun `getCurrentUserPushConfig no push key`() = runTest { + val unifiedPushProvider = createUnifiedPushProvider( + appNavigationStateService = FakeAppNavigationStateService( + appNavigationState = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Session(owner = "owner", sessionId = A_SESSION_ID), + isInForeground = true + ) + ) + ), + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = { A_SECRET } + ), + unifiedPushStore = FakeUnifiedPushStore( + getPushGatewayResult = { "aPushGateway" }, + getEndpointResult = { null } + ), + ) + val result = unifiedPushProvider.getCurrentUserPushConfig() + assertThat(result).isNull() + } + + @Test + fun `getCurrentUserPushConfig ok`() = runTest { + val unifiedPushProvider = createUnifiedPushProvider( + appNavigationStateService = FakeAppNavigationStateService( + appNavigationState = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Session(owner = "owner", sessionId = A_SESSION_ID), + isInForeground = true + ) + ) + ), + pushClientSecret = FakePushClientSecret( + getSecretForUserResult = { A_SECRET } + ), + unifiedPushStore = FakeUnifiedPushStore( + getPushGatewayResult = { "aPushGateway" }, + getEndpointResult = { "aEndpoint" } + ), + ) + val result = unifiedPushProvider.getCurrentUserPushConfig() + assertThat(result).isEqualTo(CurrentUserPushConfig("aPushGateway", "aEndpoint")) + } + + private fun createUnifiedPushProvider( + unifiedPushDistributorProvider: UnifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(), + registerUnifiedPushUseCase: RegisterUnifiedPushUseCase = FakeRegisterUnifiedPushUseCase(), + unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(), + pushClientSecret: PushClientSecret = FakePushClientSecret(), + unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(), + appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), + ): UnifiedPushProvider { + return UnifiedPushProvider( + unifiedPushDistributorProvider = unifiedPushDistributorProvider, + registerUnifiedPushUseCase = registerUnifiedPushUseCase, + unRegisterUnifiedPushUseCase = unRegisterUnifiedPushUseCase, + pushClientSecret = pushClientSecret, + unifiedPushStore = unifiedPushStore, + appNavigationStateService = appNavigationStateService + ) + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt new file mode 100644 index 00000000000..e2054caacb2 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/VectorUnifiedPushMessagingReceiverTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.libraries.pushproviders.unifiedpush + +import androidx.test.platform.app.InstrumentationRegistry +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SECRET +import io.element.android.libraries.push.test.test.FakePushHandler +import io.element.android.libraries.pushproviders.api.PushData +import io.element.android.libraries.pushproviders.api.PushHandler +import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler +import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class VectorUnifiedPushMessagingReceiverTest { + @Test + fun `onUnregistered does nothing`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver() + vectorUnifiedPushMessagingReceiver.onUnregistered(context, A_SECRET) + } + + @Test + fun `onRegistrationFailed does nothing`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver() + vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, A_SECRET) + } + + @Test + fun `onMessage valid invoke the push handler`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val pushHandlerResult = lambdaRecorder {} + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( + pushHandler = FakePushHandler( + handleResult = pushHandlerResult + ), + ) + vectorUnifiedPushMessagingReceiver.onMessage(context, UnifiedPushParserTest.UNIFIED_PUSH_DATA.toByteArray(), A_SECRET) + advanceUntilIdle() + pushHandlerResult.assertions() + .isCalledOnce() + .with( + value( + PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + clientSecret = A_SECRET + ) + ) + ) + } + + @Test + fun `onMessage invalid does not invoke the push handler`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val pushHandlerResult = lambdaRecorder {} + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( + pushHandler = FakePushHandler( + handleResult = pushHandlerResult + ), + ) + vectorUnifiedPushMessagingReceiver.onMessage(context, "".toByteArray(), A_SECRET) + advanceUntilIdle() + pushHandlerResult.assertions() + .isNeverCalled() + } + + @Test + fun `onNewEndpoint run the expected tasks`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val storePushGatewayResult = lambdaRecorder { _, _ -> } + val storeUpEndpointResult = lambdaRecorder { _, _ -> } + val unifiedPushStore = FakeUnifiedPushStore( + storePushGatewayResult = storePushGatewayResult, + storeUpEndpointResult = storeUpEndpointResult, + ) + val endpointRegistrationHandler = EndpointRegistrationHandler() + val handleResult = lambdaRecorder> { _, _, _ -> Result.success(Unit) } + val unifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler( + handleResult = handleResult + ) + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( + unifiedPushStore = unifiedPushStore, + unifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver( + getGatewayResult = { "aGateway" } + ), + endpointRegistrationHandler = endpointRegistrationHandler, + unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler, + ) + endpointRegistrationHandler.state.test { + vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET) + advanceUntilIdle() + assertThat(awaitItem()).isEqualTo( + RegistrationResult( + clientSecret = A_SECRET, + result = Result.success(Unit) + ) + ) + } + storePushGatewayResult.assertions() + .isCalledOnce() + .with(value(A_SECRET), value("aGateway")) + storeUpEndpointResult.assertions() + .isCalledOnce() + .with(value(A_SECRET), value("anEndpoint")) + } + + @Test + fun `onNewEndpoint, if registration fails, the endpoint should not be stored`() = runTest { + val context = InstrumentationRegistry.getInstrumentation().context + val storePushGatewayResult = lambdaRecorder { _, _ -> } + val storeUpEndpointResult = lambdaRecorder { _, _ -> } + val unifiedPushStore = FakeUnifiedPushStore( + storePushGatewayResult = storePushGatewayResult, + storeUpEndpointResult = storeUpEndpointResult, + ) + val endpointRegistrationHandler = EndpointRegistrationHandler() + val handleResult = lambdaRecorder> { _, _, _ -> Result.failure(AN_EXCEPTION) } + val unifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler( + handleResult = handleResult + ) + val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver( + unifiedPushStore = unifiedPushStore, + unifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver( + getGatewayResult = { "aGateway" } + ), + endpointRegistrationHandler = endpointRegistrationHandler, + unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler, + ) + endpointRegistrationHandler.state.test { + vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, "anEndpoint", A_SECRET) + advanceUntilIdle() + assertThat(awaitItem()).isEqualTo( + RegistrationResult( + clientSecret = A_SECRET, + result = Result.failure(AN_EXCEPTION) + ) + ) + } + storePushGatewayResult.assertions() + .isCalledOnce() + .with(value(A_SECRET), value("aGateway")) + storeUpEndpointResult.assertions() + .isNeverCalled() + } + + private fun TestScope.createVectorUnifiedPushMessagingReceiver( + pushHandler: PushHandler = FakePushHandler(), + unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(), + unifiedPushGatewayResolver: UnifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver(), + unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(), + endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(), + ): VectorUnifiedPushMessagingReceiver { + return VectorUnifiedPushMessagingReceiver().apply { + this.pushParser = UnifiedPushParser() + this.pushHandler = pushHandler + this.guardServiceStarter = NoopGuardServiceStarter() + this.unifiedPushStore = unifiedPushStore + this.unifiedPushGatewayResolver = unifiedPushGatewayResolver + this.newGatewayHandler = unifiedPushNewGatewayHandler + this.endpointRegistrationHandler = endpointRegistrationHandler + this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver + } + } +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt index 117e8b74575..9f79f7363b5 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/pushproviders/unifiedpush/troubleshoot/UnifiedPushTestTest.kt @@ -19,7 +19,9 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.pushproviders.api.Distributor +import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState +import io.element.android.libraries.troubleshoot.api.test.TestFilterData import io.element.android.services.toolbox.test.strings.FakeStringProvider import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest @@ -81,4 +83,15 @@ class UnifiedPushTestTest { assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) } } + + @Test + fun `test isRelevant`() { + val sut = UnifiedPushTest( + unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(), + openDistributorWebPageAction = FakeOpenDistributorWebPageAction(), + stringProvider = FakeStringProvider(), + ) + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = UnifiedPushConfig.NAME))).isTrue() + assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "other"))).isFalse() + } } diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 28e53e011ce..69f0f21e553 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.services.appnavstate.test) + testImplementation(projects.libraries.pushstore.test) testImplementation(projects.libraries.sessionStorage.test) androidTestImplementation(libs.coroutines.test) diff --git a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt index dc0e5b3651f..0277feef452 100644 --- a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.pushstore.impl.clientsecret import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.InMemoryPushClientSecretStore import io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver import kotlinx.coroutines.test.runTest import org.junit.Test diff --git a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt similarity index 94% rename from libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt rename to libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt index 3428fb5d3e3..112a752368f 100644 --- a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt +++ b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStore.kt @@ -14,14 +14,15 @@ * limitations under the License. */ -package com.element.android.libraries.pushstore.test.userpushstore +package io.element.android.libraries.pushstore.test.userpushstore import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -class FakeUserPushStore : UserPushStore { +class FakeUserPushStore( private var pushProviderName: String? = null +) : UserPushStore { private var currentRegisteredPushKey: String? = null private val notificationEnabledForDevice = MutableStateFlow(true) override suspend fun getPushProviderName(): String? { diff --git a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt similarity index 78% rename from libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt rename to libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt index 2f4f524cc2e..14fd4ce3a63 100644 --- a/libraries/pushstore/test/src/main/kotlin/com/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt +++ b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/FakeUserPushStoreFactory.kt @@ -14,14 +14,16 @@ * limitations under the License. */ -package com.element.android.libraries.pushstore.test.userpushstore +package io.element.android.libraries.pushstore.test.userpushstore import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory -class FakeUserPushStoreFactory : UserPushStoreFactory { +class FakeUserPushStoreFactory( + val userPushStore: (SessionId) -> UserPushStore = { FakeUserPushStore() } +) : UserPushStoreFactory { override fun getOrCreate(userId: SessionId): UserPushStore { - return FakeUserPushStore() + return userPushStore(userId) } } diff --git a/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/FakePushClientSecret.kt b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/FakePushClientSecret.kt new file mode 100644 index 00000000000..25759ecc45d --- /dev/null +++ b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/FakePushClientSecret.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushstore.test.userpushstore.clientsecret + +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.tests.testutils.lambda.lambdaError + +class FakePushClientSecret( + private val getSecretForUserResult: (SessionId) -> String = { lambdaError() }, + private val getUserIdFromSecretResult: (String) -> SessionId? = { lambdaError() } +) : PushClientSecret { + override suspend fun getSecretForUser(userId: SessionId): String { + return getSecretForUserResult(userId) + } + + override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? { + return getUserIdFromSecretResult(clientSecret) + } +} diff --git a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/InMemoryPushClientSecretStore.kt similarity index 94% rename from libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt rename to libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/InMemoryPushClientSecretStore.kt index 8c9b5779678..632014109eb 100644 --- a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt +++ b/libraries/pushstore/test/src/main/kotlin/io/element/android/libraries/pushstore/test/userpushstore/clientsecret/InMemoryPushClientSecretStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.pushstore.impl.clientsecret +package io.element.android.libraries.pushstore.test.userpushstore.clientsecret import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore diff --git a/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.kt b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.kt new file mode 100644 index 00000000000..5331d5755b1 --- /dev/null +++ b/libraries/session-storage/impl-memory/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/memory/InMemoryMultiSessionsStore.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.sessionstorage.impl.memory + +import io.element.android.libraries.sessionstorage.api.LoggedInState +import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.sessionstorage.api.SessionStore +import kotlinx.coroutines.flow.Flow + +class InMemoryMultiSessionsStore : SessionStore { + private val sessions = mutableListOf() + + override fun isLoggedIn(): Flow = error("Not implemented") + + override fun sessionsFlow(): Flow> = error("Not implemented") + + override suspend fun storeData(sessionData: SessionData) { + sessions.add(sessionData) + } + + override suspend fun updateData(sessionData: SessionData) = error("Not implemented") + + override suspend fun getSession(sessionId: String): SessionData? = error("Not implemented") + + override suspend fun getAllSessions(): List = sessions + + override suspend fun getLatestSession(): SessionData = error("Not implemented") + + override suspend fun removeSession(sessionId: String) = error("Not implemented") +} diff --git a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt index f7da518feba..e5c9f4793c3 100644 --- a/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt +++ b/libraries/textcomposer/impl/src/main/kotlin/io/element/android/libraries/textcomposer/mentions/MentionSpanProvider.kt @@ -164,7 +164,7 @@ internal fun MentionSpanPreview() { eventId = null, viaParameters = persistentListOf(), ) - else -> TODO() + else -> throw AssertionError("Unexpected value $uriString") } } }, diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt index f2f2c31fed9..aaf7dc54a18 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/EnsureNeverCalled.kt @@ -16,26 +16,28 @@ package io.element.android.tests.testutils +import io.element.android.tests.testutils.lambda.lambdaError + class EnsureNeverCalled : () -> Unit { override fun invoke() { - throw AssertionError("Should not be called") + lambdaError() } } class EnsureNeverCalledWithParam : (T) -> Unit { override fun invoke(p1: T) { - throw AssertionError("Should not be called and is called with $p1") + lambdaError("Should not be called and is called with $p1") } } class EnsureNeverCalledWithParamAndResult : (T) -> R { override fun invoke(p1: T): R { - throw AssertionError("Should not be called and is called with $p1") + lambdaError("Should not be called and is called with $p1") } } class EnsureNeverCalledWithTwoParams : (T, U) -> Unit { override fun invoke(p1: T, p2: U) { - throw AssertionError("Should not be called and is called with $p1 and $p2") + lambdaError("Should not be called and is called with $p1 and $p2") } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt new file mode 100644 index 00000000000..c7fc10495ac --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/Error.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils.lambda + +fun lambdaError( + message: String = "This lambda should never be called." +): Nothing { + throw AssertionError(message) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt index 4e49560398e..8455d34ef6e 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/lambda/LambdaRecorder.kt @@ -26,7 +26,7 @@ abstract class LambdaRecorder internal constructor( internal fun onInvoke(vararg params: Any?) { if (assertNoInvocation) { - throw AssertionError("This lambda should never be called.") + lambdaError() } parametersSequence.add(params.toList()) }