Merge pull request #2899 from element-hq/feature/bma/unitTestPush

Add unit tests in some push classes
This commit is contained in:
Benoit Marty 2024-05-23 14:35:25 +02:00 committed by GitHub
commit 498e8e7897
87 changed files with 3396 additions and 285 deletions

View file

@ -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_SESSION_ID
import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID
import io.element.android.libraries.matrix.test.permalink.FakePermalinkParser 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.Assert.assertThrows
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -229,7 +230,7 @@ class IntentResolverTest {
} }
private fun createIntentResolver( private fun createIntentResolver(
permalinkParserResult: () -> PermalinkData = { throw NotImplementedError() } permalinkParserResult: () -> PermalinkData = { lambdaError() }
): IntentResolver { ): IntentResolver {
return IntentResolver( return IntentResolver(
deeplinkParser = DeeplinkParser(), deeplinkParser = DeeplinkParser(),

View file

@ -192,9 +192,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
} }
assert(joinRoomFailure) assert(joinRoomFailure)
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite)) value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
) )
} }
@ -221,9 +223,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents() cancelAndConsumeRemainingEvents()
} }
assert(joinRoomSuccess) assert(joinRoomSuccess)
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite)) value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
) )
} }

View file

@ -19,12 +19,12 @@ package io.element.android.features.preferences.impl.notifications
import app.cash.molecule.RecompositionMode import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomNotificationMode 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.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService 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 io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View file

@ -136,7 +136,7 @@ class BugReportPresenterTest {
initialState.eventSink.invoke(BugReportEvents.ResetAll) initialState.eventSink.invoke(BugReportEvents.ResetAll)
val resetState = awaitItem() val resetState = awaitItem()
assertThat(resetState.hasCrashLogs).isFalse() assertThat(resetState.hasCrashLogs).isFalse()
logFilesRemoverLambda.assertions().isCalledExactly(1) logFilesRemoverLambda.assertions().isCalledOnce()
// TODO Make it live assertThat(resetState.screenshotUri).isNull() // TODO Make it live assertThat(resetState.screenshotUri).isNull()
} }
} }

View file

@ -57,9 +57,9 @@ class DefaultJoinRoomTest {
.isNeverCalled() .isNeverCalled()
joinRoomLambda joinRoomLambda
.assertions() .assertions()
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID)) value(A_ROOM_ID)
) )
assertThat(analyticsService.capturedEvents).containsExactly( assertThat(analyticsService.capturedEvents).containsExactly(
roomResult.toAnalyticsJoinedRoom(aTrigger) roomResult.toAnalyticsJoinedRoom(aTrigger)
@ -88,9 +88,10 @@ class DefaultJoinRoomTest {
sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger) sut.invoke(A_ROOM_ID, A_SERVER_LIST, aTrigger)
joinRoomByIdOrAliasLambda joinRoomByIdOrAliasLambda
.assertions() .assertions()
.isCalledExactly(1) .isCalledOnce()
.withSequence( .with(
listOf(value(A_ROOM_ID), value(A_SERVER_LIST)) value(A_ROOM_ID),
value(A_SERVER_LIST)
) )
joinRoomLambda joinRoomLambda
.assertions() .assertions()

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationSettings
const val A_USER_NAME = "alice" const val A_USER_NAME = "alice"
const val A_PASSWORD = "password" const val A_PASSWORD = "password"
const val A_SECRET = "secret"
val A_USER_ID = UserId("@alice:server.org") val A_USER_ID = UserId("@alice:server.org")
val A_USER_ID_2 = UserId("@bob:server.org") val A_USER_ID_2 = UserId("@bob:server.org")

View file

@ -31,7 +31,9 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url") val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService { class FakeAuthenticationService(
private val matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null
) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null) private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null private var oidcCancelError: Throwable? = null
@ -48,6 +50,9 @@ class FakeAuthenticationService : MatrixAuthenticationService {
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda() override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> { override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
if (matrixClientResult != null) {
return matrixClientResult.invoke(sessionId)
}
return if (matrixClient != null) { return if (matrixClient != null) {
Result.success(matrixClient!!) Result.success(matrixClient!!)
} else { } else {

View file

@ -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.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser import io.element.android.libraries.matrix.api.permalink.PermalinkParser
import io.element.android.tests.testutils.lambda.lambdaError
class FakePermalinkParser( class FakePermalinkParser(
private var result: () -> PermalinkData = { TODO("Not implemented") } private var result: () -> PermalinkData = { lambdaError() }
) : PermalinkParser { ) : PermalinkParser {
fun givenResult(result: PermalinkData) { fun givenResult(result: PermalinkData) {
this.result = { result } this.result = { result }

View file

@ -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.PushersService
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushersService : PushersService { class FakePushersService(
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) private val setHttpPusherResult: (SetHttpPusherData) -> Result<Unit> = { lambdaError() },
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> = Result.success(Unit) private val unsetHttpPusherResult: (UnsetHttpPusherData) -> Result<Unit> = { lambdaError() },
) : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = setHttpPusherResult(setHttpPusherData)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> = unsetHttpPusherResult(unsetHttpPusherData)
} }

View file

@ -21,9 +21,6 @@ import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
interface PushService { interface PushService {
// TODO Move away
fun notificationStyleChanged()
/** /**
* Return the current push provider, or null if none. * Return the current push provider, or null if none.
*/ */

View file

@ -72,6 +72,7 @@ dependencies {
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushproviders.test) testImplementation(projects.libraries.pushproviders.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.impl)

View file

@ -21,7 +21,7 @@ import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.push.api.GetCurrentPushProvider import io.element.android.libraries.push.api.GetCurrentPushProvider
import io.element.android.libraries.push.api.PushService 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.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
@ -30,16 +30,11 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPushService @Inject constructor( class DefaultPushService @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, private val testPush: TestPush,
private val pushersManager: PushersManager,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>,
private val getCurrentPushProvider: GetCurrentPushProvider, private val getCurrentPushProvider: GetCurrentPushProvider,
) : PushService { ) : PushService {
override fun notificationStyleChanged() {
defaultNotificationDrawerManager.notificationStyleChanged()
}
override suspend fun getCurrentPushProvider(): PushProvider? { override suspend fun getCurrentPushProvider(): PushProvider? {
val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider() val currentPushProvider = getCurrentPushProvider.getCurrentPushProvider()
return pushProviders.find { it.name == currentPushProvider } return pushProviders.find { it.name == currentPushProvider }
@ -51,9 +46,6 @@ class DefaultPushService @Inject constructor(
.sortedBy { it.index } .sortedBy { it.index }
} }
/**
* Get current push provider, compare with provided one, then unregister and register if different, and store change.
*/
override suspend fun registerWith( override suspend fun registerWith(
matrixClient: MatrixClient, matrixClient: MatrixClient,
pushProvider: PushProvider, pushProvider: PushProvider,
@ -80,7 +72,7 @@ class DefaultPushService @Inject constructor(
override suspend fun testPush(): Boolean { override suspend fun testPush(): Boolean {
val pushProvider = getCurrentPushProvider() ?: return false val pushProvider = getCurrentPushProvider() ?: return false
val config = pushProvider.getCurrentUserPushConfig() ?: return false val config = pushProvider.getCurrentUserPushConfig() ?: return false
pushersManager.testPush(config) testPush.execute(config)
return true return true
} }
} }

View file

@ -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.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient 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.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData 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.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret 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" 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) @ContributesBinding(AppScope::class)
class PushersManager @Inject constructor( class DefaultPusherSubscriber @Inject constructor(
// private val localeProvider: LocaleProvider,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
private val pushClientSecret: PushClientSecret, private val pushClientSecret: PushClientSecret,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
) : PusherSubscriber { ) : 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. * 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") 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")
}
} }

View file

@ -19,7 +19,9 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.content.FileProvider 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.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider 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 timber.log.Timber
import javax.inject.Inject 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. * 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, * 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. * 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 stringProvider: StringProvider,
private val clock: SystemClock, private val clock: SystemClock,
private val matrixClientProvider: MatrixClientProvider, private val matrixClientProvider: MatrixClientProvider,
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory, private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) { ) : NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session // Restore session
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val notificationService = client.notificationService() val notificationService = client.notificationService()

View file

@ -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( private fun updateEvents(
doRender: Boolean, doRender: Boolean,
action: (NotificationEventQueue) -> Unit, action: (NotificationEventQueue) -> Unit,

View file

@ -16,27 +16,19 @@
package io.element.android.libraries.push.impl.push package io.element.android.libraries.push.impl.push
import android.os.Handler
import android.os.Looper
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService 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.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.push.impl.troubleshoot.DiagnosticPushHandler
import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushData
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret 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.flow.first
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -44,23 +36,15 @@ private val loggerTag = LoggerTag("PushHandler", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPushHandler @Inject constructor( class DefaultPushHandler @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager, private val onNotifiableEventReceived: OnNotifiableEventReceived,
private val notifiableEventResolver: NotifiableEventResolver, private val notifiableEventResolver: NotifiableEventResolver,
private val defaultPushDataStore: DefaultPushDataStore, private val incrementPushDataStore: IncrementPushDataStore,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret, private val pushClientSecret: PushClientSecret,
// private val actionIds: NotificationActionIds,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
private val diagnosticPushHandler: DiagnosticPushHandler, private val diagnosticPushHandler: DiagnosticPushHandler,
) : PushHandler { ) : PushHandler {
private val coroutineScope = CoroutineScope(SupervisorJob())
// UI handler
private val uiHandler by lazy {
Handler(Looper.getMainLooper())
}
/** /**
* Called when message is received. * Called when message is received.
* *
@ -68,21 +52,15 @@ class DefaultPushHandler @Inject constructor(
*/ */
override suspend fun handle(pushData: PushData) { override suspend fun handle(pushData: PushData) {
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}") Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
if (buildMeta.lowPrivacyLoggingEnabled) { if (buildMeta.lowPrivacyLoggingEnabled) {
Timber.tag(loggerTag.value).d("## pushData: $pushData") Timber.tag(loggerTag.value).d("## pushData: $pushData")
} }
incrementPushDataStore.incrementPushCounter()
defaultPushDataStore.incrementPushCounter()
// Diagnostic Push // Diagnostic Push
if (pushData.eventId == PushersManager.TEST_EVENT_ID) { if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
diagnosticPushHandler.handlePush() diagnosticPushHandler.handlePush()
return } else {
} handleInternal(pushData)
uiHandler.post {
coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) }
} }
} }
@ -98,7 +76,6 @@ class DefaultPushHandler @Inject constructor(
} else { } else {
Timber.tag(loggerTag.value).d("## handleInternal()") Timber.tag(loggerTag.value).d("## handleInternal()")
} }
val clientSecret = pushData.clientSecret val clientSecret = pushData.clientSecret
// clientSecret should not be null. If this happens, restore default session // clientSecret should not be null. If this happens, restore default session
val userId = clientSecret val userId = clientSecret
@ -109,27 +86,22 @@ class DefaultPushHandler @Inject constructor(
?: run { ?: run {
matrixAuthenticationService.getLatestSessionId() matrixAuthenticationService.getLatestSessionId()
} }
if (userId == null) { if (userId == null) {
Timber.w("Unable to get a session") Timber.w("Unable to get a session")
return 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) 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 // TODO We need to check if this is an incoming call
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
return
} }
defaultNotificationDrawerManager.onNotifiableEventReceived(notifiableEvent)
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
} }

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.pushgateway
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.POST import retrofit2.http.POST
internal interface PushGatewayAPI { interface PushGatewayAPI {
/** /**
* Ask the Push Gateway to send a push to the current device. * Ask the Push Gateway to send a push to the current device.
* *

View file

@ -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)
}
}

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class PushGatewayDevice( data class PushGatewayDevice(
/** /**
* Required. The app_id given when the pusher was created. * Required. The app_id given when the pusher was created.
*/ */

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class PushGatewayNotification( data class PushGatewayNotification(
@SerialName("event_id") @SerialName("event_id")
val eventId: String, val eventId: String,
@SerialName("room_id") @SerialName("room_id")

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class PushGatewayNotifyBody( data class PushGatewayNotifyBody(
/** /**
* Required. Information about the push notification * Required. Information about the push notification
*/ */

View file

@ -15,15 +15,14 @@
*/ */
package io.element.android.libraries.push.impl.pushgateway 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.EventId
import io.element.android.libraries.matrix.api.core.RoomId 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 io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject import javax.inject.Inject
class PushGatewayNotifyRequest @Inject constructor( interface PushGatewayNotifyRequest {
private val retrofitFactory: RetrofitFactory,
) {
data class Params( data class Params(
val url: String, val url: String,
val appId: String, val appId: String,
@ -32,13 +31,18 @@ class PushGatewayNotifyRequest @Inject constructor(
val roomId: RoomId, val roomId: RoomId,
) )
suspend fun execute(params: Params) { suspend fun execute(params: Params)
val sygnalApi = retrofitFactory.create( }
@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) params.url.substringBefore(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH)
) )
.create(PushGatewayAPI::class.java) val response = pushGatewayApi.notify(
val response = sygnalApi.notify(
PushGatewayNotifyBody( PushGatewayNotifyBody(
PushGatewayNotification( PushGatewayNotification(
eventId = params.eventId.value, eventId = params.eventId.value,

View file

@ -20,7 +20,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class PushGatewayNotifyResponse( data class PushGatewayNotifyResponse(
@SerialName("rejected") @SerialName("rejected")
val rejectedPushKeys: List<String> val rejectedPushKeys: List<String>
) )

View file

@ -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")
}
}

View file

@ -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<CurrentUserPushConfig, Unit> { }
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<MatrixClient, Result<Unit>> { Result.success(Unit) }
val registerLambda = lambdaRecorder<MatrixClient, Distributor, Result<Unit>> { _, _ -> 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,
)
}
}

View file

@ -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<Unit>,
) {
val setHttpPusherResult = lambdaRecorder<SetHttpPusherData, Result<Unit>> { 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<Unit>,
) {
val unsetHttpPusherResult = lambdaRecorder<UnsetHttpPusherData, Result<Unit>> { 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,
)
}
}

View file

@ -59,17 +59,17 @@ import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class NotifiableEventResolverTest { class DefaultNotifiableEventResolverTest {
@Test @Test
fun `resolve event no session`() = runTest { 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) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
assertThat(result).isNull() assertThat(result).isNull()
} }
@Test @Test
fun `resolve event failure`() = runTest { fun `resolve event failure`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.failure(AN_EXCEPTION) notificationResult = Result.failure(AN_EXCEPTION)
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
@ -78,7 +78,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event null`() = runTest { fun `resolve event null`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success(null) notificationResult = Result.success(null)
) )
val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID) val result = sut.resolveEvent(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID)
@ -87,7 +87,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message text`() = runTest { fun `resolve event message text`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -105,7 +105,7 @@ class NotifiableEventResolverTest {
@Test @Test
@Config(qualifiers = "en") @Config(qualifiers = "en")
fun `resolve event message with mention`() = runTest { fun `resolve event message with mention`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -123,7 +123,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve HTML formatted event message text takes plain text version`() = runTest { fun `resolve HTML formatted event message text takes plain text version`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -146,7 +146,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve incorrectly formatted event message text uses fallback`() = runTest { fun `resolve incorrectly formatted event message text uses fallback`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -169,7 +169,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message audio`() = runTest { fun `resolve event message audio`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -186,7 +186,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message video`() = runTest { fun `resolve event message video`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -203,7 +203,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message voice`() = runTest { fun `resolve event message voice`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -220,7 +220,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message image`() = runTest { fun `resolve event message image`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -237,7 +237,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message sticker`() = runTest { fun `resolve event message sticker`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -254,7 +254,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message file`() = runTest { fun `resolve event message file`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -271,7 +271,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message location`() = runTest { fun `resolve event message location`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -288,7 +288,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message notice`() = runTest { fun `resolve event message notice`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -305,7 +305,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve event message emote`() = runTest { fun `resolve event message emote`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomMessage( content = NotificationContent.MessageLike.RoomMessage(
@ -322,7 +322,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve poll`() = runTest { fun `resolve poll`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.Poll( content = NotificationContent.MessageLike.Poll(
@ -339,7 +339,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve RoomMemberContent invite room`() = runTest { fun `resolve RoomMemberContent invite room`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent( content = NotificationContent.StateEvent.RoomMemberContent(
@ -372,7 +372,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve RoomMemberContent invite direct`() = runTest { fun `resolve RoomMemberContent invite direct`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent( content = NotificationContent.StateEvent.RoomMemberContent(
@ -405,7 +405,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve RoomMemberContent other`() = runTest { fun `resolve RoomMemberContent other`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.StateEvent.RoomMemberContent( content = NotificationContent.StateEvent.RoomMemberContent(
@ -421,7 +421,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve RoomEncrypted`() = runTest { fun `resolve RoomEncrypted`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.RoomEncrypted content = NotificationContent.MessageLike.RoomEncrypted
@ -445,7 +445,7 @@ class NotifiableEventResolverTest {
@Test @Test
fun `resolve CallInvite`() = runTest { fun `resolve CallInvite`() = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2) content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2)
@ -517,7 +517,7 @@ class NotifiableEventResolverTest {
} }
private fun testNull(content: NotificationContent) = runTest { private fun testNull(content: NotificationContent) = runTest {
val sut = createNotifiableEventResolver( val sut = createDefaultNotifiableEventResolver(
notificationResult = Result.success( notificationResult = Result.success(
createNotificationData( createNotificationData(
content = content content = content
@ -528,10 +528,10 @@ class NotifiableEventResolverTest {
assertThat(result).isNull() assertThat(result).isNull()
} }
private fun createNotifiableEventResolver( private fun createDefaultNotifiableEventResolver(
notificationService: FakeNotificationService? = FakeNotificationService(), notificationService: FakeNotificationService? = FakeNotificationService(),
notificationResult: Result<NotificationData?> = Result.success(null), notificationResult: Result<NotificationData?> = Result.success(null),
): NotifiableEventResolver { ): DefaultNotifiableEventResolver {
val context = RuntimeEnvironment.getApplication() as Context val context = RuntimeEnvironment.getApplication() as Context
notificationService?.givenGetNotificationResult(notificationResult) notificationService?.givenGetNotificationResult(notificationResult)
val matrixClientProvider = FakeMatrixClientProvider(getClient = { val matrixClientProvider = FakeMatrixClientProvider(getClient = {
@ -544,7 +544,7 @@ class NotifiableEventResolverTest {
val notificationMediaRepoFactory = NotificationMediaRepo.Factory { val notificationMediaRepoFactory = NotificationMediaRepo.Factory {
FakeNotificationMediaRepo() FakeNotificationMediaRepo()
} }
return NotifiableEventResolver( return DefaultNotifiableEventResolver(
stringProvider = AndroidStringProvider(context.resources), stringProvider = AndroidStringProvider(context.resources),
clock = FakeSystemClock(), clock = FakeSystemClock(),
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,

View file

@ -59,7 +59,6 @@ class DefaultNotificationDrawerManagerTest {
fun `cover all APIs`() = runTest { fun `cover all APIs`() = runTest {
// For now just call all the API. Later, add more valuable tests. // For now just call all the API. Later, add more valuable tests.
val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager()
defaultNotificationDrawerManager.notificationStyleChanged()
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true) defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true)
defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false) defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false)
defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true) defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true)

View file

@ -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)
}
}

View file

@ -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<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
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<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
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<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
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<SessionId, RoomId, EventId, NotifiableEvent> { _, _, _ -> aNotifiableMessageEvent }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
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<SessionId, RoomId, EventId, NotifiableEvent?> { _, _, _ -> null }
val onNotifiableEventReceived = lambdaRecorder<NotifiableEvent, Unit> {}
val incrementPushCounterResult = lambdaRecorder<Unit> {}
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,
)
}
}

View file

@ -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)
}
}

View file

@ -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")
}
}

View file

@ -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()
}
}

View file

@ -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<PushGatewayNotifyRequest.Params, Unit> { }
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,
)
)
)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -29,9 +29,6 @@ class FakePushService(
Result.success(Unit) Result.success(Unit)
}, },
) : PushService { ) : PushService {
override fun notificationStyleChanged() {
}
override suspend fun getCurrentPushProvider(): PushProvider? { override suspend fun getCurrentPushProvider(): PushProvider? {
return availablePushProviders.firstOrNull() return availablePushProviders.firstOrNull()
} }

View file

@ -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<Unit> = { _, _, _ -> lambdaError() },
private val unregisterPusherResult: (MatrixClient, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() },
) : PusherSubscriber {
override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit> {
return registerPusherResult(matrixClient, pushKey, gateway)
}
override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String): Result<Unit> {
return unregisterPusherResult(matrixClient, pushKey, gateway)
}
}

View file

@ -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)
}
}

View file

@ -58,7 +58,11 @@ dependencies {
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test) 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.tests.testutils)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
} }

View file

@ -16,8 +16,10 @@
package io.element.android.libraries.pushproviders.firebase 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.extensions.flatMap
import io.element.android.libraries.core.log.logger.LoggerTag 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.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushproviders.api.PusherSubscriber 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. * 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 pusherSubscriber: PusherSubscriber,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
private val firebaseStore: FirebaseStore, private val firebaseStore: FirebaseStore,
) { ) : FirebaseNewTokenHandler {
suspend fun handle(firebaseToken: String) { override suspend fun handle(firebaseToken: String) {
firebaseStore.storeFcmToken(firebaseToken) firebaseStore.storeFcmToken(firebaseToken)
// Register the pusher for all the sessions // Register the pusher for all the sessions
sessionStore.getAllSessions().toUserList() sessionStore.getAllSessions().toUserList()
@ -53,14 +60,15 @@ class FirebaseNewTokenHandler @Inject constructor(
Timber.tag(loggerTag.value).e(it, "Failed to restore session $sessionId") Timber.tag(loggerTag.value).e(it, "Failed to restore session $sessionId")
} }
.flatMap { client -> .flatMap { client ->
pusherSubscriber.registerPusher( pusherSubscriber
matrixClient = client, .registerPusher(
pushKey = firebaseToken, matrixClient = client,
gateway = FirebaseConfig.PUSHER_HTTP_URL, pushKey = firebaseToken,
) gateway = FirebaseConfig.PUSHER_HTTP_URL,
} )
.onFailure { .onFailure {
Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId") Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId")
}
} }
} else { } else {
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")

View file

@ -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.core.log.logger.LoggerTag
import io.element.android.libraries.pushproviders.api.PushHandler import io.element.android.libraries.pushproviders.api.PushHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -33,8 +32,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
@Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushParser: FirebasePushParser
@Inject lateinit var pushHandler: PushHandler @Inject lateinit var pushHandler: PushHandler
@Inject lateinit var coroutineScope: CoroutineScope
private val coroutineScope = CoroutineScope(SupervisorJob())
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View file

@ -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<MatrixClient, String, String, Result<Unit>> { _, _, _ -> 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<MatrixClient, String, String, Result<Unit>> { _, _, _ -> 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<MatrixClient, String, String, Result<Unit>> { _, _, _ -> 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,
)
}
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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<MatrixClient, String, String, Result<Unit>> { _, _, _ -> 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<MatrixClient, String, String, Result<Unit>> { _, _, _ -> 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,
)
}
}

View file

@ -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<PushData, Unit>(ensureNeverCalled = true) { }
val vectorFirebaseMessagingService = createVectorFirebaseMessagingService(
pushHandler = FakePushHandler(handleResult = lambda)
)
vectorFirebaseMessagingService.onMessageReceived(RemoteMessage(Bundle()))
}
@Test
fun `test receiving valid data`() = runTest {
val lambda = lambdaRecorder<PushData, Unit> { }
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<String, Unit> { }
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
}
}
}

View file

@ -18,8 +18,10 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat 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.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -29,11 +31,7 @@ class FirebaseAvailabilityTestTest {
@Test @Test
fun `test FirebaseAvailabilityTest success`() = runTest { fun `test FirebaseAvailabilityTest success`() = runTest {
val sut = FirebaseAvailabilityTest( val sut = FirebaseAvailabilityTest(
isPlayServiceAvailable = object : IsPlayServiceAvailable { isPlayServiceAvailable = FakeIsPlayServiceAvailable(true),
override fun isAvailable(): Boolean {
return true
}
},
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { launch {
@ -50,11 +48,7 @@ class FirebaseAvailabilityTestTest {
@Test @Test
fun `test FirebaseAvailabilityTest failure`() = runTest { fun `test FirebaseAvailabilityTest failure`() = runTest {
val sut = FirebaseAvailabilityTest( val sut = FirebaseAvailabilityTest(
isPlayServiceAvailable = object : IsPlayServiceAvailable { isPlayServiceAvailable = FakeIsPlayServiceAvailable(false),
override fun isAvailable(): Boolean {
return false
}
},
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
) )
launch { launch {
@ -67,4 +61,14 @@ class FirebaseAvailabilityTestTest {
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(false)) 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()
}
} }

View file

@ -19,8 +19,10 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter 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.pushproviders.firebase.InMemoryFirebaseStore
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState 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 io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest 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 { companion object {
private const val FAKE_TOKEN = "abcdefghijk" private const val FAKE_TOKEN = "abcdefghijk"
} }

View file

@ -24,4 +24,5 @@ android {
dependencies { dependencies {
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.pushproviders.api)
implementation(projects.tests.testutils)
} }

View file

@ -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.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushProvider( class FakePushProvider(
override val index: Int = 0, override val index: Int = 0,
override val name: String = "aFakePushProvider", override val name: String = "aFakePushProvider",
private val isAvailable: Boolean = true, private val isAvailable: Boolean = true,
private val distributors: List<Distributor> = listOf(Distributor("aDistributorValue", "aDistributorName")), private val distributors: List<Distributor> = listOf(Distributor("aDistributorValue", "aDistributorName")),
private val currentUserPushConfig: CurrentUserPushConfig? = null,
private val registerWithResult: (MatrixClient, Distributor) -> Result<Unit> = { _, _ -> lambdaError() },
private val unregisterWithResult: (MatrixClient) -> Result<Unit> = { lambdaError() },
) : PushProvider { ) : PushProvider {
override fun isAvailable(): Boolean = isAvailable override fun isAvailable(): Boolean = isAvailable
override fun getDistributors(): List<Distributor> = distributors override fun getDistributors(): List<Distributor> = distributors
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> { override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
return Result.success(Unit) return registerWithResult(matrixClient, distributor)
} }
override suspend fun getCurrentDistributor(matrixClient: MatrixClient): Distributor? { override suspend fun getCurrentDistributor(matrixClient: MatrixClient): Distributor? {
@ -40,10 +44,10 @@ class FakePushProvider(
} }
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> { override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
return Result.success(Unit) return unregisterWithResult(matrixClient)
} }
override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? { override suspend fun getCurrentUserPushConfig(): CurrentUserPushConfig? {
return null return currentUserPushConfig
} }
} }

View file

@ -55,9 +55,13 @@ dependencies {
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(libs.test.junit) testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.tests.testutils) testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test) testImplementation(projects.services.toolbox.test)
testImplementation(projects.services.appnavstate.test)
} }

View file

@ -17,43 +17,41 @@
package io.element.android.libraries.pushproviders.unifiedpush package io.element.android.libraries.pushproviders.unifiedpush
import android.content.Context 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.di.ApplicationContext
import io.element.android.libraries.pushproviders.api.Distributor 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.EndpointRegistrationHandler
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class RegisterUnifiedPushUseCase @Inject constructor( interface RegisterUnifiedPushUseCase {
suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit>
}
@ContributesBinding(AppScope::class)
class DefaultRegisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val endpointRegistrationHandler: EndpointRegistrationHandler, private val endpointRegistrationHandler: EndpointRegistrationHandler,
private val coroutineScope: CoroutineScope, ) : RegisterUnifiedPushUseCase {
) { override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
UnifiedPush.saveDistributor(context, distributor.value) UnifiedPush.saveDistributor(context, distributor.value)
val completable = CompletableDeferred<Result<Unit>>()
val job = coroutineScope.launch {
val result = endpointRegistrationHandler.state
.filter { it.clientSecret == clientSecret }
.first()
.result
completable.complete(result)
}
// This will trigger the callback // This will trigger the callback
// VectorUnifiedPushMessagingReceiver.onNewEndpoint // VectorUnifiedPushMessagingReceiver.onNewEndpoint
UnifiedPush.registerApp(context = context, instance = clientSecret) UnifiedPush.registerApp(context = context, instance = clientSecret)
// Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed // Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed
return withTimeout(30.seconds) { return runCatching {
completable.await() withTimeout(30.seconds) {
} val result = endpointRegistrationHandler.state
.onFailure { .filter { it.clientSecret == clientSecret }
job.cancel() .first()
.result
result.getOrThrow()
} }
}
} }
} }

View file

@ -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)
}
}

View file

@ -16,29 +16,33 @@
package io.element.android.libraries.pushproviders.unifiedpush 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.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.network.RetrofitFactory import io.element.android.libraries.di.AppScope
import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class UnifiedPushGatewayResolver @Inject constructor( interface UnifiedPushGatewayResolver {
private val retrofitFactory: RetrofitFactory, suspend fun getGateway(endpoint: String): String
}
@ContributesBinding(AppScope::class)
class DefaultUnifiedPushGatewayResolver @Inject constructor(
private val unifiedPushApiFactory: UnifiedPushApiFactory,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
) { ) : UnifiedPushGatewayResolver {
suspend fun getGateway(endpoint: String): String? { override suspend fun getGateway(endpoint: String): String {
val gateway = UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL 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 { 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) { return withContext(coroutineDispatchers.io) {
val api = retrofitFactory.create(customBase) val api = unifiedPushApiFactory.create(customBase)
.create(UnifiedPushApi::class.java)
try { try {
val discoveryResponse = api.discover() val discoveryResponse = api.discover()
if (discoveryResponse.unifiedpush.gateway == "matrix") { if (discoveryResponse.unifiedpush.gateway == "matrix") {

View file

@ -16,8 +16,10 @@
package io.element.android.libraries.pushproviders.unifiedpush 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.extensions.flatMap
import io.element.android.libraries.core.log.logger.LoggerTag 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.auth.MatrixAuthenticationService
import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory 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 timber.log.Timber
import javax.inject.Inject 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. * 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<Unit>
}
@ContributesBinding(AppScope::class)
class DefaultUnifiedPushNewGatewayHandler @Inject constructor(
private val pusherSubscriber: PusherSubscriber, private val pusherSubscriber: PusherSubscriber,
private val userPushStoreFactory: UserPushStoreFactory, private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret, private val pushClientSecret: PushClientSecret,
private val matrixAuthenticationService: MatrixAuthenticationService, private val matrixAuthenticationService: MatrixAuthenticationService,
) { ) : UnifiedPushNewGatewayHandler {
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> { override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
// Register the pusher for the session with this client secret, if is it using UnifiedPush. // Register the pusher for the session with this client secret, if is it using UnifiedPush.
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>( val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
IllegalStateException("Unable to retrieve session") IllegalStateException("Unable to retrieve session")

View file

@ -18,7 +18,6 @@ package io.element.android.libraries.pushproviders.unifiedpush
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushproviders.api.PushData
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import javax.inject.Inject import javax.inject.Inject

View file

@ -19,32 +19,44 @@ package io.element.android.libraries.pushproviders.unifiedpush
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit 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.ApplicationContext
import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.DefaultPreferences
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import javax.inject.Inject 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, @ApplicationContext val context: Context,
@DefaultPreferences private val defaultPrefs: SharedPreferences, @DefaultPreferences private val defaultPrefs: SharedPreferences,
) { ) : UnifiedPushStore {
/** /**
* Retrieves the UnifiedPush Endpoint. * Retrieves the UnifiedPush Endpoint.
* *
* @param clientSecret the client secret, to identify the session * @param clientSecret the client secret, to identify the session
* @return the UnifiedPush Endpoint or null if not received * @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) return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
} }
/** /**
* Store UnifiedPush Endpoint to the SharedPrefs. * Store UnifiedPush Endpoint to the SharedPrefs.
* *
* @param endpoint the endpoint to store
* @param clientSecret the client secret, to identify the session * @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 { defaultPrefs.edit {
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint) putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
} }
@ -56,27 +68,27 @@ class UnifiedPushStore @Inject constructor(
* @param clientSecret the client secret, to identify the session * @param clientSecret the client secret, to identify the session
* @return the Push Gateway or null if not defined * @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) return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
} }
/** /**
* Store Push Gateway to the SharedPrefs. * Store Push Gateway to the SharedPrefs.
* *
* @param gateway the push gateway to store
* @param clientSecret the client secret, to identify the session * @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 { defaultPrefs.edit {
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway) putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
} }
} }
fun getDistributorValue(userId: UserId): String? { override fun getDistributorValue(userId: UserId): String? {
return defaultPrefs.getString(PREFS_DISTRIBUTOR + userId, null) return defaultPrefs.getString(PREFS_DISTRIBUTOR + userId, null)
} }
fun setDistributorValue(userId: UserId, value: String) { override fun setDistributorValue(userId: UserId, value: String) {
defaultPrefs.edit { defaultPrefs.edit {
putString(PREFS_DISTRIBUTOR + userId, value) putString(PREFS_DISTRIBUTOR + userId, value)
} }

View file

@ -17,18 +17,25 @@
package io.element.android.libraries.pushproviders.unifiedpush package io.element.android.libraries.pushproviders.unifiedpush
import android.content.Context 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.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.pushproviders.api.PusherSubscriber import io.element.android.libraries.pushproviders.api.PusherSubscriber
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
import javax.inject.Inject import javax.inject.Inject
class UnregisterUnifiedPushUseCase @Inject constructor( interface UnregisterUnifiedPushUseCase {
suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result<Unit>
}
@ContributesBinding(AppScope::class)
class DefaultUnregisterUnifiedPushUseCase @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val unifiedPushStore: UnifiedPushStore, private val unifiedPushStore: UnifiedPushStore,
private val pusherSubscriber: PusherSubscriber, private val pusherSubscriber: PusherSubscriber,
) { ) : UnregisterUnifiedPushUseCase {
suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result<Unit> { override suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result<Unit> {
val endpoint = unifiedPushStore.getEndpoint(clientSecret) val endpoint = unifiedPushStore.getEndpoint(clientSecret)
val gateway = unifiedPushStore.getPushGateway(clientSecret) val gateway = unifiedPushStore.getPushGateway(clientSecret)
if (endpoint == null || gateway == null) { if (endpoint == null || gateway == null) {
@ -36,8 +43,8 @@ class UnregisterUnifiedPushUseCase @Inject constructor(
} }
return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway) return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway)
.onSuccess { .onSuccess {
unifiedPushStore.storeUpEndpoint(null, clientSecret) unifiedPushStore.storeUpEndpoint(clientSecret, null)
unifiedPushStore.storePushGateway(null, clientSecret) unifiedPushStore.storePushGateway(clientSecret, null)
UnifiedPush.unregisterApp(context) UnifiedPush.unregisterApp(context)
} }
} }

View file

@ -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.EndpointRegistrationHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.unifiedpush.android.connector.MessagingReceiver import org.unifiedpush.android.connector.MessagingReceiver
import timber.log.Timber import timber.log.Timber
@ -40,8 +39,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler @Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
@Inject lateinit var coroutineScope: CoroutineScope
private val coroutineScope = CoroutineScope(SupervisorJob())
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this) context.applicationContext.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
@ -75,30 +73,20 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
Timber.tag(loggerTag.value).i("onNewEndpoint: $endpoint") Timber.tag(loggerTag.value).i("onNewEndpoint: $endpoint")
coroutineScope.launch { coroutineScope.launch {
val gateway = unifiedPushGatewayResolver.getGateway(endpoint) val gateway = unifiedPushGatewayResolver.getGateway(endpoint)
unifiedPushStore.storePushGateway(gateway, instance) unifiedPushStore.storePushGateway(instance, gateway)
if (gateway == null) { val result = newGatewayHandler.handle(endpoint, gateway, instance)
Timber.tag(loggerTag.value).w("No gateway found for endpoint $endpoint") .onFailure {
endpointRegistrationHandler.registrationDone( Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway")
RegistrationResult( }
clientSecret = instance, .onSuccess {
result = Result.failure(IllegalStateException("No gateway found for endpoint $endpoint")), unifiedPushStore.storeUpEndpoint(instance, endpoint)
) }
endpointRegistrationHandler.registrationDone(
RegistrationResult(
clientSecret = instance,
result = result,
) )
} 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,
)
)
}
} }
guardServiceStarter.stop() guardServiceStarter.stop()
} }

View file

@ -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,
)
}
}

View file

@ -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()
)
}

View file

@ -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
)
}
}

View file

@ -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
)
}
}

View file

@ -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<Unit> = { _, _ -> lambdaError() }
) : RegisterUnifiedPushUseCase {
override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
return result(distributor, clientSecret)
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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<Unit> = { _, _, _ -> lambdaError() },
) : UnifiedPushNewGatewayHandler {
override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
return handleResult(endpoint, pushGateway, clientSecret)
}
}

View file

@ -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)
}
}

View file

@ -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<Unit> = { _, _ -> lambdaError() }
) : UnregisterUnifiedPushUseCase {
override suspend fun execute(matrixClient: MatrixClient, clientSecret: String): Result<Unit> {
return result(matrixClient, clientSecret)
}
}

View file

@ -82,9 +82,8 @@ class UnifiedPushParserTest {
} }
companion object { 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\"}}" "{\"notification\":{\"event_id\":\"$AN_EVENT_ID\",\"room_id\":\"$A_ROOM_ID\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}"
// TODO Check client secret format?
} }
} }

View file

@ -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<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<Distributor, String, Result<Unit>> { _, _ -> Result.success(Unit) }
val setDistributorValueResultLambda = lambdaRecorder<UserId, String, Unit> { _, _ -> }
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<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<Distributor, String, Result<Unit>> { _, _ -> Result.failure(AN_EXCEPTION) }
val setDistributorValueResultLambda = lambdaRecorder<UserId, String, Unit>(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<Unit>(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<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<MatrixClient, String, Result<Unit>> { _, _ -> 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<SessionId, String> { A_SECRET }
val executeLambda = lambdaRecorder<MatrixClient, String, Result<Unit>> { _, _ -> 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<Unit>(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
)
}
}

View file

@ -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<PushData, Unit> {}
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<PushData, Unit> {}
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<String, String?, Unit> { _, _ -> }
val storeUpEndpointResult = lambdaRecorder<String, String?, Unit> { _, _ -> }
val unifiedPushStore = FakeUnifiedPushStore(
storePushGatewayResult = storePushGatewayResult,
storeUpEndpointResult = storeUpEndpointResult,
)
val endpointRegistrationHandler = EndpointRegistrationHandler()
val handleResult = lambdaRecorder<String, String, String, Result<Unit>> { _, _, _ -> 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<String, String?, Unit> { _, _ -> }
val storeUpEndpointResult = lambdaRecorder<String, String?, Unit> { _, _ -> }
val unifiedPushStore = FakeUnifiedPushStore(
storePushGatewayResult = storePushGatewayResult,
storeUpEndpointResult = storeUpEndpointResult,
)
val endpointRegistrationHandler = EndpointRegistrationHandler()
val handleResult = lambdaRecorder<String, String, String, Result<Unit>> { _, _, _ -> 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
}
}
}

View file

@ -19,7 +19,9 @@ package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.Distributor 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.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -81,4 +83,15 @@ class UnifiedPushTestTest {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success) 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()
}
} }

View file

@ -49,6 +49,7 @@ dependencies {
testImplementation(libs.coroutines.test) testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.sessionStorage.test) testImplementation(projects.libraries.sessionStorage.test)
androidTestImplementation(libs.coroutines.test) androidTestImplementation(libs.coroutines.test)

View file

@ -18,6 +18,7 @@ package io.element.android.libraries.pushstore.impl.clientsecret
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId 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 io.element.android.libraries.sessionstorage.test.observer.NoOpSessionObserver
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test

View file

@ -14,14 +14,15 @@
* limitations under the License. * 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 io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeUserPushStore : UserPushStore { class FakeUserPushStore(
private var pushProviderName: String? = null private var pushProviderName: String? = null
) : UserPushStore {
private var currentRegisteredPushKey: String? = null private var currentRegisteredPushKey: String? = null
private val notificationEnabledForDevice = MutableStateFlow(true) private val notificationEnabledForDevice = MutableStateFlow(true)
override suspend fun getPushProviderName(): String? { override suspend fun getPushProviderName(): String? {

View file

@ -14,14 +14,16 @@
* limitations under the License. * 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.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory 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 { override fun getOrCreate(userId: SessionId): UserPushStore {
return FakeUserPushStore() return userPushStore(userId)
} }
} }

View file

@ -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)
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * 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.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore

View file

@ -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<SessionData>()
override fun isLoggedIn(): Flow<LoggedInState> = error("Not implemented")
override fun sessionsFlow(): Flow<List<SessionData>> = 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<SessionData> = sessions
override suspend fun getLatestSession(): SessionData = error("Not implemented")
override suspend fun removeSession(sessionId: String) = error("Not implemented")
}

View file

@ -164,7 +164,7 @@ internal fun MentionSpanPreview() {
eventId = null, eventId = null,
viaParameters = persistentListOf(), viaParameters = persistentListOf(),
) )
else -> TODO() else -> throw AssertionError("Unexpected value $uriString")
} }
} }
}, },

View file

@ -16,26 +16,28 @@
package io.element.android.tests.testutils package io.element.android.tests.testutils
import io.element.android.tests.testutils.lambda.lambdaError
class EnsureNeverCalled : () -> Unit { class EnsureNeverCalled : () -> Unit {
override fun invoke() { override fun invoke() {
throw AssertionError("Should not be called") lambdaError()
} }
} }
class EnsureNeverCalledWithParam<T> : (T) -> Unit { class EnsureNeverCalledWithParam<T> : (T) -> Unit {
override fun invoke(p1: T) { 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> : (T) -> R { class EnsureNeverCalledWithParamAndResult<T, R> : (T) -> R {
override fun invoke(p1: 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> : (T, U) -> Unit { class EnsureNeverCalledWithTwoParams<T, U> : (T, U) -> Unit {
override fun invoke(p1: T, p2: U) { 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")
} }
} }

View file

@ -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)
}

View file

@ -26,7 +26,7 @@ abstract class LambdaRecorder internal constructor(
internal fun onInvoke(vararg params: Any?) { internal fun onInvoke(vararg params: Any?) {
if (assertNoInvocation) { if (assertNoInvocation) {
throw AssertionError("This lambda should never be called.") lambdaError()
} }
parametersSequence.add(params.toList()) parametersSequence.add(params.toList())
} }