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

View File

@@ -192,9 +192,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents()
}
assert(joinRoomFailure)
.isCalledExactly(1)
.withSequence(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite))
.isCalledOnce()
.with(
value(A_ROOM_ID),
value(emptyList<String>()),
value(JoinedRoom.Trigger.Invite)
)
}
@@ -221,9 +223,11 @@ class AcceptDeclineInvitePresenterTest {
cancelAndConsumeRemainingEvents()
}
assert(joinRoomSuccess)
.isCalledExactly(1)
.withSequence(
listOf(value(A_ROOM_ID), value(emptyList<String>()), value(JoinedRoom.Trigger.Invite))
.isCalledOnce()
.with(
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.moleculeFlow
import app.cash.turbine.test
import com.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Test

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,9 @@ import kotlinx.coroutines.flow.flowOf
val A_OIDC_DATA = OidcDetails(url = "a-url")
class FakeAuthenticationService : MatrixAuthenticationService {
class FakeAuthenticationService(
private val matrixClientResult: ((SessionId) -> Result<MatrixClient>)? = null
) : MatrixAuthenticationService {
private val homeserver = MutableStateFlow<MatrixHomeServerDetails?>(null)
private var oidcError: Throwable? = null
private var oidcCancelError: Throwable? = null
@@ -48,6 +50,9 @@ class FakeAuthenticationService : MatrixAuthenticationService {
override suspend fun getLatestSessionId(): SessionId? = getLatestSessionIdLambda()
override suspend fun restoreSession(sessionId: SessionId): Result<MatrixClient> {
if (matrixClientResult != null) {
return matrixClientResult.invoke(sessionId)
}
return if (matrixClient != null) {
Result.success(matrixClient!!)
} 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.PermalinkParser
import io.element.android.tests.testutils.lambda.lambdaError
class FakePermalinkParser(
private var result: () -> PermalinkData = { TODO("Not implemented") }
private var result: () -> PermalinkData = { lambdaError() }
) : PermalinkParser {
fun givenResult(result: PermalinkData) {
this.result = { result }

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.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.tests.testutils.lambda.lambdaError
class FakePushersService : PushersService {
override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit)
override suspend fun unsetHttpPusher(unsetHttpPusherData: UnsetHttpPusherData): Result<Unit> = Result.success(Unit)
class FakePushersService(
private val setHttpPusherResult: (SetHttpPusherData) -> Result<Unit> = { lambdaError() },
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
interface PushService {
// TODO Move away
fun notificationStyleChanged()
/**
* Return the current push provider, or null if none.
*/

View File

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

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

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.di.AppScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData
import io.element.android.libraries.matrix.api.pusher.UnsetHttpPusherData
import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest
import io.element.android.libraries.pushproviders.api.CurrentUserPushConfig
import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
@@ -37,29 +33,14 @@ import javax.inject.Inject
internal const val DEFAULT_PUSHER_FILE_TAG = "mobile"
private val loggerTag = LoggerTag("PushersManager", LoggerTag.PushLoggerTag)
private val loggerTag = LoggerTag("DefaultPusherSubscriber", LoggerTag.PushLoggerTag)
@ContributesBinding(AppScope::class)
class PushersManager @Inject constructor(
// private val localeProvider: LocaleProvider,
class DefaultPusherSubscriber @Inject constructor(
private val buildMeta: BuildMeta,
// private val getDeviceInfoUseCase: GetDeviceInfoUseCase,
private val pushGatewayNotifyRequest: PushGatewayNotifyRequest,
private val pushClientSecret: PushClientSecret,
private val userPushStoreFactory: UserPushStoreFactory,
) : PusherSubscriber {
suspend fun testPush(config: CurrentUserPushConfig) {
pushGatewayNotifyRequest.execute(
PushGatewayNotifyRequest.Params(
url = config.url,
appId = PushConfig.PUSHER_APP_ID,
pushKey = config.pushKey,
eventId = TEST_EVENT_ID,
roomId = TEST_ROOM_ID,
)
)
}
/**
* Register a pusher to the server if not done yet.
*/
@@ -131,9 +112,4 @@ class PushersManager @Inject constructor(
Timber.tag(loggerTag.value).e(throwable, "Unable to unregister the pusher")
}
}
companion object {
val TEST_EVENT_ID = EventId("\$THIS_IS_A_FAKE_EVENT_ID")
val TEST_ROOM_ID = RoomId("!room:domain")
}
}

View File

@@ -19,7 +19,9 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
@@ -55,7 +57,7 @@ import io.element.android.services.toolbox.api.systemclock.SystemClock
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.NotificationLoggerTag)
private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.NotificationLoggerTag)
/**
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
@@ -63,15 +65,20 @@ private val loggerTag = LoggerTag("NotifiableEventResolver", LoggerTag.Notificat
* The NotifiableEventResolver is the only aware of session/store, the NotificationDrawerManager has no knowledge of that,
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
*/
class NotifiableEventResolver @Inject constructor(
interface NotifiableEventResolver {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent?
}
@ContributesBinding(AppScope::class)
class DefaultNotifiableEventResolver @Inject constructor(
private val stringProvider: StringProvider,
private val clock: SystemClock,
private val matrixClientProvider: MatrixClientProvider,
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
@ApplicationContext private val context: Context,
private val permalinkParser: PermalinkParser,
) {
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
) : NotifiableEventResolver {
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? {
// Restore session
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
val notificationService = client.notificationService()

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

View File

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

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.POST
internal interface PushGatewayAPI {
interface PushGatewayAPI {
/**
* 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
@Serializable
internal data class PushGatewayDevice(
data class PushGatewayDevice(
/**
* Required. The app_id given when the pusher was created.
*/

View File

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

View File

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

View File

@@ -15,15 +15,14 @@
*/
package io.element.android.libraries.push.impl.pushgateway
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.push.api.gateway.PushGatewayFailure
import javax.inject.Inject
class PushGatewayNotifyRequest @Inject constructor(
private val retrofitFactory: RetrofitFactory,
) {
interface PushGatewayNotifyRequest {
data class Params(
val url: String,
val appId: String,
@@ -32,13 +31,18 @@ class PushGatewayNotifyRequest @Inject constructor(
val roomId: RoomId,
)
suspend fun execute(params: Params) {
val sygnalApi = retrofitFactory.create(
suspend fun execute(params: Params)
}
@ContributesBinding(AppScope::class)
class DefaultPushGatewayNotifyRequest @Inject constructor(
private val pushGatewayApiFactory: PushGatewayApiFactory,
) : PushGatewayNotifyRequest {
override suspend fun execute(params: PushGatewayNotifyRequest.Params) {
val pushGatewayApi = pushGatewayApiFactory.create(
params.url.substringBefore(PushGatewayConfig.URI_PUSH_GATEWAY_PREFIX_PATH)
)
.create(PushGatewayAPI::class.java)
val response = sygnalApi.notify(
val response = pushGatewayApi.notify(
PushGatewayNotifyBody(
PushGatewayNotification(
eventId = params.eventId.value,

View File

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

View File

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

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)
},
) : PushService {
override fun notificationStyleChanged() {
}
override suspend fun getCurrentPushProvider(): PushProvider? {
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.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.push.test)
testImplementation(projects.libraries.pushstore.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
}

View File

@@ -16,8 +16,10 @@
package io.element.android.libraries.pushproviders.firebase
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushproviders.api.PusherSubscriber
@@ -32,14 +34,19 @@ private val loggerTag = LoggerTag("FirebaseNewTokenHandler", LoggerTag.PushLogge
/**
* Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider.
*/
class FirebaseNewTokenHandler @Inject constructor(
interface FirebaseNewTokenHandler {
suspend fun handle(firebaseToken: String)
}
@ContributesBinding(AppScope::class)
class DefaultFirebaseNewTokenHandler @Inject constructor(
private val pusherSubscriber: PusherSubscriber,
private val sessionStore: SessionStore,
private val userPushStoreFactory: UserPushStoreFactory,
private val matrixAuthenticationService: MatrixAuthenticationService,
private val firebaseStore: FirebaseStore,
) {
suspend fun handle(firebaseToken: String) {
) : FirebaseNewTokenHandler {
override suspend fun handle(firebaseToken: String) {
firebaseStore.storeFcmToken(firebaseToken)
// Register the pusher for all the sessions
sessionStore.getAllSessions().toUserList()
@@ -53,14 +60,15 @@ class FirebaseNewTokenHandler @Inject constructor(
Timber.tag(loggerTag.value).e(it, "Failed to restore session $sessionId")
}
.flatMap { client ->
pusherSubscriber.registerPusher(
matrixClient = client,
pushKey = firebaseToken,
gateway = FirebaseConfig.PUSHER_HTTP_URL,
)
}
.onFailure {
Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId")
pusherSubscriber
.registerPusher(
matrixClient = client,
pushKey = firebaseToken,
gateway = FirebaseConfig.PUSHER_HTTP_URL,
)
.onFailure {
Timber.tag(loggerTag.value).e(it, "Failed to register pusher for session $sessionId")
}
}
} else {
Timber.tag(loggerTag.value).d("This session is not using Firebase pusher")

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

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

View File

@@ -19,8 +19,10 @@ package io.element.android.libraries.pushproviders.firebase.troubleshoot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.firebase.FakeFirebaseTroubleshooter
import io.element.android.libraries.pushproviders.firebase.FirebaseConfig
import io.element.android.libraries.pushproviders.firebase.InMemoryFirebaseStore
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
@@ -75,6 +77,17 @@ class FirebaseTokenTestTest {
}
}
@Test
fun `test FirebaseTokenTest isRelevant`() {
val sut = FirebaseTokenTest(
firebaseStore = InMemoryFirebaseStore(null),
firebaseTroubleshooter = FakeFirebaseTroubleshooter(),
stringProvider = FakeStringProvider(),
)
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "unknown"))).isFalse()
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = FirebaseConfig.NAME))).isTrue()
}
companion object {
private const val FAKE_TOKEN = "abcdefghijk"
}

View File

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

View File

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

View File

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

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
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.network.RetrofitFactory
import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi
import io.element.android.libraries.di.AppScope
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.URL
import javax.inject.Inject
class UnifiedPushGatewayResolver @Inject constructor(
private val retrofitFactory: RetrofitFactory,
interface UnifiedPushGatewayResolver {
suspend fun getGateway(endpoint: String): String
}
@ContributesBinding(AppScope::class)
class DefaultUnifiedPushGatewayResolver @Inject constructor(
private val unifiedPushApiFactory: UnifiedPushApiFactory,
private val coroutineDispatchers: CoroutineDispatchers,
) {
suspend fun getGateway(endpoint: String): String? {
) : UnifiedPushGatewayResolver {
override suspend fun getGateway(endpoint: String): String {
val gateway = UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL
val url = URL(endpoint)
val port = if (url.port != -1) ":${url.port}" else ""
val customBase = "${url.protocol}://${url.host}$port"
val customUrl = "$customBase/_matrix/push/v1/notify"
Timber.i("Testing $customUrl")
try {
val url = URL(endpoint)
val port = if (url.port != -1) ":${url.port}" else ""
val customBase = "${url.protocol}://${url.host}$port"
val customUrl = "$customBase/_matrix/push/v1/notify"
Timber.i("Testing $customUrl")
return withContext(coroutineDispatchers.io) {
val api = retrofitFactory.create(customBase)
.create(UnifiedPushApi::class.java)
val api = unifiedPushApiFactory.create(customBase)
try {
val discoveryResponse = api.discover()
if (discoveryResponse.unifiedpush.gateway == "matrix") {

View File

@@ -16,8 +16,10 @@
package io.element.android.libraries.pushproviders.unifiedpush
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.extensions.flatMap
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.pushproviders.api.PusherSubscriber
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
@@ -25,18 +27,23 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
import timber.log.Timber
import javax.inject.Inject
private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag)
private val loggerTag = LoggerTag("DefaultUnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag)
/**
* Handle new endpoint received from UnifiedPush. Will update the session matching the client secret.
*/
class UnifiedPushNewGatewayHandler @Inject constructor(
interface UnifiedPushNewGatewayHandler {
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit>
}
@ContributesBinding(AppScope::class)
class DefaultUnifiedPushNewGatewayHandler @Inject constructor(
private val pusherSubscriber: PusherSubscriber,
private val userPushStoreFactory: UserPushStoreFactory,
private val pushClientSecret: PushClientSecret,
private val matrixAuthenticationService: MatrixAuthenticationService,
) {
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
) : UnifiedPushNewGatewayHandler {
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.
val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
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.pushproviders.api.PushData
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import javax.inject.Inject

View File

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

View File

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

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

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 {
private val UNIFIED_PUSH_DATA =
val UNIFIED_PUSH_DATA =
"{\"notification\":{\"event_id\":\"$AN_EVENT_ID\",\"room_id\":\"$A_ROOM_ID\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}"
// TODO Check client secret format?
}
}

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 com.google.common.truth.Truth.assertThat
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
@@ -81,4 +83,15 @@ class UnifiedPushTestTest {
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
}
}
@Test
fun `test isRelevant`() {
val sut = UnifiedPushTest(
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(),
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(),
stringProvider = FakeStringProvider(),
)
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = UnifiedPushConfig.NAME))).isTrue()
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "other"))).isFalse()
}
}

View File

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

View File

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

View File

@@ -14,14 +14,15 @@
* limitations under the License.
*/
package com.element.android.libraries.pushstore.test.userpushstore
package io.element.android.libraries.pushstore.test.userpushstore
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeUserPushStore : UserPushStore {
class FakeUserPushStore(
private var pushProviderName: String? = null
) : UserPushStore {
private var currentRegisteredPushKey: String? = null
private val notificationEnabledForDevice = MutableStateFlow(true)
override suspend fun getPushProviderName(): String? {

View File

@@ -14,14 +14,16 @@
* limitations under the License.
*/
package com.element.android.libraries.pushstore.test.userpushstore
package io.element.android.libraries.pushstore.test.userpushstore
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
class FakeUserPushStoreFactory : UserPushStoreFactory {
class FakeUserPushStoreFactory(
val userPushStore: (SessionId) -> UserPushStore = { FakeUserPushStore() }
) : UserPushStoreFactory {
override fun getOrCreate(userId: SessionId): UserPushStore {
return FakeUserPushStore()
return userPushStore(userId)
}
}

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.
*/
package io.element.android.libraries.pushstore.impl.clientsecret
package io.element.android.libraries.pushstore.test.userpushstore.clientsecret
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore

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,
viaParameters = persistentListOf(),
)
else -> TODO()
else -> throw AssertionError("Unexpected value $uriString")
}
}
},

View File

@@ -16,26 +16,28 @@
package io.element.android.tests.testutils
import io.element.android.tests.testutils.lambda.lambdaError
class EnsureNeverCalled : () -> Unit {
override fun invoke() {
throw AssertionError("Should not be called")
lambdaError()
}
}
class EnsureNeverCalledWithParam<T> : (T) -> Unit {
override fun invoke(p1: T) {
throw AssertionError("Should not be called and is called with $p1")
lambdaError("Should not be called and is called with $p1")
}
}
class EnsureNeverCalledWithParamAndResult<T, R> : (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 {
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?) {
if (assertNoInvocation) {
throw AssertionError("This lambda should never be called.")
lambdaError()
}
parametersSequence.add(params.toList())
}