First Commit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.pushproviders.unifiedpush"
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.push.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
api(projects.libraries.troubleshoot.api)
|
||||
|
||||
implementation(projects.libraries.pushstore.api)
|
||||
implementation(projects.libraries.pushproviders.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.services.toolbox.api)
|
||||
|
||||
implementation(projects.libraries.network)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp.okhttp)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.network.retrofit)
|
||||
|
||||
implementation(libs.serialization.json)
|
||||
|
||||
// UnifiedPush library
|
||||
api(libs.unifiedpush)
|
||||
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(libs.kotlinx.collections.immutable)
|
||||
testImplementation(projects.features.enterprise.test)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.libraries.pushproviders.test)
|
||||
testImplementation(projects.libraries.pushstore.test)
|
||||
testImplementation(projects.libraries.troubleshoot.test)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2025 Element Creations Ltd.
|
||||
~ Copyright 2023 New Vector Ltd.
|
||||
~
|
||||
~ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
~ Please see LICENSE files in the repository root for full details.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application>
|
||||
<receiver
|
||||
android:name=".VectorUnifiedPushMessagingReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE" />
|
||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED" />
|
||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT" />
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED" />
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".KeepInternalDistributor"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<!--
|
||||
This action is checked to track installed and uninstalled distributors.
|
||||
We declare it to keep the background sync as an internal
|
||||
unifiedpush distributor.
|
||||
-->
|
||||
<action android:name="org.unifiedpush.android.distributor.REGISTER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
|
||||
interface DefaultPushGatewayHttpUrlProvider {
|
||||
fun provide(): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultDefaultPushGatewayHttpUrlProvider(
|
||||
private val enterpriseService: EnterpriseService,
|
||||
) : DefaultPushGatewayHttpUrlProvider {
|
||||
override fun provide(): String {
|
||||
return enterpriseService.unifiedPushDefaultPushGateway() ?: UnifiedPushConfig.DEFAULT_PUSH_GATEWAY_HTTP_URL
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
||||
interface GuardServiceStarter {
|
||||
fun start() {}
|
||||
fun stop() {}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class NoopGuardServiceStarter : GuardServiceStarter
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
/**
|
||||
* UnifiedPush lib tracks an action to check installed and uninstalled distributors.
|
||||
* We declare it to keep the background sync as an internal unifiedpush distributor.
|
||||
* This class is used to declare this action.
|
||||
*/
|
||||
class KeepInternalDistributor : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* In this case, the format is:
|
||||
* <pre>
|
||||
* {
|
||||
* "notification":{
|
||||
* "event_id":"$anEventId",
|
||||
* "room_id":"!aRoomId",
|
||||
* "counts":{
|
||||
* "unread":1
|
||||
* },
|
||||
* "prio":"high"
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* .
|
||||
*/
|
||||
@Serializable
|
||||
data class PushDataUnifiedPush(
|
||||
val notification: PushDataUnifiedPushNotification? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushNotification(
|
||||
@SerialName("event_id") val eventId: String? = null,
|
||||
@SerialName("room_id") val roomId: String? = null,
|
||||
@SerialName("counts") val counts: PushDataUnifiedPushCounts? = null,
|
||||
@SerialName("prio") val prio: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PushDataUnifiedPushCounts(
|
||||
@SerialName("unread") val unread: Int? = null
|
||||
)
|
||||
|
||||
fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? {
|
||||
val safeEventId = notification?.eventId?.let(::EventId) ?: return null
|
||||
val safeRoomId = notification.roomId?.let(::RoomId) ?: return null
|
||||
return PushData(
|
||||
eventId = safeEventId,
|
||||
roomId = safeRoomId,
|
||||
unread = notification.counts?.unread,
|
||||
clientSecret = clientSecret
|
||||
)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
interface RegisterUnifiedPushUseCase {
|
||||
suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultRegisterUnifiedPushUseCase(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val endpointRegistrationHandler: EndpointRegistrationHandler,
|
||||
) : RegisterUnifiedPushUseCase {
|
||||
override suspend fun execute(distributor: Distributor, clientSecret: String): Result<Unit> {
|
||||
UnifiedPush.saveDistributor(context, distributor.value)
|
||||
// This will trigger the callback
|
||||
// VectorUnifiedPushMessagingReceiver.onNewEndpoint
|
||||
UnifiedPush.register(context = context, instance = clientSecret)
|
||||
// Wait for VectorUnifiedPushMessagingReceiver.onNewEndpoint to proceed
|
||||
@Suppress("RunCatchingNotAllowed")
|
||||
return runCatching {
|
||||
withTimeout(30.seconds) {
|
||||
val result = endpointRegistrationHandler.state
|
||||
.filter { it.clientSecret == clientSecret }
|
||||
.first()
|
||||
.result
|
||||
result.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.network.RetrofitFactory
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.network.UnifiedPushApi
|
||||
|
||||
interface UnifiedPushApiFactory {
|
||||
fun create(baseUrl: String): UnifiedPushApi
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushApiFactory(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
) : UnifiedPushApiFactory {
|
||||
override fun create(baseUrl: String): UnifiedPushApi {
|
||||
return retrofitFactory.create(baseUrl)
|
||||
.create(UnifiedPushApi::class.java)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
object UnifiedPushConfig {
|
||||
/**
|
||||
* It is the push gateway for UnifiedPush.
|
||||
* Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify'
|
||||
*/
|
||||
const val DEFAULT_PUSH_GATEWAY_HTTP_URL: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify"
|
||||
|
||||
const val UNIFIED_PUSH_DISTRIBUTORS_URL = "https://unifiedpush.org/users/distributors/"
|
||||
|
||||
const val INDEX = 1
|
||||
const val NAME = "UnifiedPush"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.getApplicationLabel
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
|
||||
interface UnifiedPushDistributorProvider {
|
||||
fun getDistributors(): List<Distributor>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushDistributorProvider(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : UnifiedPushDistributorProvider {
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
val distributors = UnifiedPush.getDistributors(context)
|
||||
return distributors.mapNotNull {
|
||||
if (it == context.packageName) {
|
||||
// Exclude self
|
||||
null
|
||||
} else {
|
||||
Distributor(it, context.getApplicationLabel(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import kotlinx.coroutines.withContext
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
sealed interface UnifiedPushGatewayResolverResult {
|
||||
data class Success(val gateway: String) : UnifiedPushGatewayResolverResult
|
||||
data class Error(val gateway: String) : UnifiedPushGatewayResolverResult
|
||||
data object NoMatrixGateway : UnifiedPushGatewayResolverResult
|
||||
data object ErrorInvalidUrl : UnifiedPushGatewayResolverResult
|
||||
}
|
||||
|
||||
interface UnifiedPushGatewayResolver {
|
||||
suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult
|
||||
}
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultUnifiedPushGatewayResolver")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushGatewayResolver(
|
||||
private val unifiedPushApiFactory: UnifiedPushApiFactory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : UnifiedPushGatewayResolver {
|
||||
override suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult {
|
||||
val url = tryOrNull(
|
||||
onException = { Timber.tag(loggerTag.value).d(it, "Cannot parse endpoint as an URL") }
|
||||
) {
|
||||
URL(endpoint)
|
||||
}
|
||||
return if (url == null) {
|
||||
Timber.tag(loggerTag.value).d("ErrorInvalidUrl")
|
||||
UnifiedPushGatewayResolverResult.ErrorInvalidUrl
|
||||
} else {
|
||||
val port = if (url.port != -1) ":${url.port}" else ""
|
||||
val customBase = "${url.protocol}://${url.host}$port"
|
||||
val customUrl = "$customBase/_matrix/push/v1/notify"
|
||||
Timber.tag(loggerTag.value).i("Testing $customUrl")
|
||||
return withContext(coroutineDispatchers.io) {
|
||||
val api = unifiedPushApiFactory.create(customBase)
|
||||
try {
|
||||
val discoveryResponse = api.discover()
|
||||
if (discoveryResponse.unifiedpush.gateway == "matrix") {
|
||||
Timber.tag(loggerTag.value).d("The endpoint seems to be a valid UnifiedPush gateway")
|
||||
UnifiedPushGatewayResolverResult.Success(customUrl)
|
||||
} else {
|
||||
// The endpoint returned a 200 OK but didn't promote an actual matrix gateway, which means it doesn't have any
|
||||
Timber.tag(loggerTag.value).w("The endpoint does not seem to be a valid UnifiedPush gateway, using fallback")
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
val code = (throwable as? HttpException)?.code()
|
||||
if (code in NoMatrixGatewayResp) {
|
||||
Timber.tag(loggerTag.value).i("Checking for UnifiedPush endpoint yielded $code, using fallback")
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).e(throwable, "Error checking for UnifiedPush endpoint")
|
||||
UnifiedPushGatewayResolverResult.Error(customUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val NoMatrixGatewayResp = listOf<Int>(
|
||||
HttpURLConnection.HTTP_UNAUTHORIZED,
|
||||
HttpURLConnection.HTTP_FORBIDDEN,
|
||||
HttpURLConnection.HTTP_NOT_FOUND,
|
||||
HttpURLConnection.HTTP_BAD_METHOD,
|
||||
HttpURLConnection.HTTP_NOT_ACCEPTABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
||||
interface UnifiedPushGatewayUrlResolver {
|
||||
fun resolve(
|
||||
gatewayResult: UnifiedPushGatewayResolverResult,
|
||||
instance: String,
|
||||
): String
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushGatewayUrlResolver(
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider,
|
||||
) : UnifiedPushGatewayUrlResolver {
|
||||
override fun resolve(
|
||||
gatewayResult: UnifiedPushGatewayResolverResult,
|
||||
instance: String,
|
||||
): String {
|
||||
return when (gatewayResult) {
|
||||
is UnifiedPushGatewayResolverResult.Error -> {
|
||||
// Use previous gateway if any, or the provided one
|
||||
unifiedPushStore.getPushGateway(instance) ?: gatewayResult.gateway
|
||||
}
|
||||
UnifiedPushGatewayResolverResult.ErrorInvalidUrl,
|
||||
UnifiedPushGatewayResolverResult.NoMatrixGateway -> {
|
||||
defaultPushGatewayHttpUrlProvider.provide()
|
||||
}
|
||||
is UnifiedPushGatewayResolverResult.Success -> {
|
||||
gatewayResult.gateway
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
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 timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultUnifiedPushNewGatewayHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle new endpoint received from UnifiedPush. Will update the session matching the client secret.
|
||||
*/
|
||||
interface UnifiedPushNewGatewayHandler {
|
||||
suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushNewGatewayHandler(
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
private val userPushStoreFactory: UserPushStoreFactory,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : 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")
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
val userDataStore = userPushStoreFactory.getOrCreate(userId)
|
||||
return if (userDataStore.getPushProviderName() == UnifiedPushConfig.NAME) {
|
||||
matrixClientProvider
|
||||
.getOrRestore(userId)
|
||||
.flatMap { client ->
|
||||
pusherSubscriber.registerPusher(client, endpoint, pushGateway)
|
||||
}
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register pusher")
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher")
|
||||
Result.failure(
|
||||
IllegalStateException("This session is not using UnifiedPush pusher")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||
import io.element.android.libraries.core.data.tryOrNull
|
||||
import io.element.android.libraries.pushproviders.api.PushData
|
||||
|
||||
@Inject
|
||||
class UnifiedPushParser(
|
||||
private val json: JsonProvider,
|
||||
) {
|
||||
fun parse(message: ByteArray, clientSecret: String): PushData? {
|
||||
return tryOrNull { json().decodeFromString<PushDataUnifiedPush>(String(message)) }?.toPushData(clientSecret)
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class UnifiedPushProvider(
|
||||
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
|
||||
private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase,
|
||||
private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val unifiedPushSessionPushConfigProvider: UnifiedPushSessionPushConfigProvider,
|
||||
) : PushProvider {
|
||||
override val index = UnifiedPushConfig.INDEX
|
||||
override val name = UnifiedPushConfig.NAME
|
||||
override val supportMultipleDistributors = true
|
||||
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return unifiedPushDistributorProvider.getDistributors()
|
||||
}
|
||||
|
||||
override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor): Result<Unit> {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||
return registerUnifiedPushUseCase.execute(distributor, clientSecret)
|
||||
.onSuccess {
|
||||
unifiedPushStore.setDistributorValue(matrixClient.sessionId, distributor.value)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributorValue(sessionId: SessionId): String? {
|
||||
return unifiedPushStore.getDistributorValue(sessionId)
|
||||
}
|
||||
|
||||
override suspend fun getCurrentDistributor(sessionId: SessionId): Distributor? {
|
||||
val distributorValue = unifiedPushStore.getDistributorValue(sessionId)
|
||||
return getDistributors().find { it.value == distributorValue }
|
||||
}
|
||||
|
||||
override suspend fun unregister(matrixClient: MatrixClient): Result<Unit> {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId)
|
||||
return unRegisterUnifiedPushUseCase.unregister(matrixClient, clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun onSessionDeleted(sessionId: SessionId) {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(sessionId)
|
||||
unRegisterUnifiedPushUseCase.cleanup(clientSecret)
|
||||
}
|
||||
|
||||
override suspend fun getPushConfig(sessionId: SessionId): Config? {
|
||||
return unifiedPushSessionPushConfigProvider.provide(sessionId)
|
||||
}
|
||||
|
||||
override fun canRotateToken(): Boolean = false
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.androidutils.throttler.FirstThrottler
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("UnifiedPushRemovedGatewayHandler", LoggerTag.PushLoggerTag)
|
||||
|
||||
/**
|
||||
* Handle endpoint removal received from UnifiedPush. Will try to register again.
|
||||
*/
|
||||
fun interface UnifiedPushRemovedGatewayHandler {
|
||||
suspend fun handle(clientSecret: String): Result<Unit>
|
||||
}
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class UnifiedPushRemovedGatewayThrottler(
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
) {
|
||||
private val firstThrottler = FirstThrottler(
|
||||
minimumInterval = 60_000,
|
||||
coroutineScope = appCoroutineScope,
|
||||
)
|
||||
|
||||
fun canRegisterAgain(): Boolean {
|
||||
return firstThrottler.canHandle()
|
||||
}
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushRemovedGatewayHandler(
|
||||
private val unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase,
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val pushService: PushService,
|
||||
private val unifiedPushRemovedGatewayThrottler: UnifiedPushRemovedGatewayThrottler,
|
||||
) : UnifiedPushRemovedGatewayHandler {
|
||||
/**
|
||||
* The application has been informed by the UnifiedPush distributor that the topic has been deleted.
|
||||
* So this code aim to unregister the pusher from the homeserver, register a new topic on the
|
||||
* UnifiedPush application then register a new pusher to the homeserver.
|
||||
* No registration will happen if the topic deletion has already occurred in the last minute.
|
||||
*/
|
||||
override suspend fun handle(clientSecret: String): Result<Unit> {
|
||||
val sessionId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Result.failure<Unit>(
|
||||
IllegalStateException("Unable to retrieve session")
|
||||
).also {
|
||||
Timber.tag(loggerTag.value).w("Unable to retrieve session")
|
||||
}
|
||||
return matrixClientProvider
|
||||
.getOrRestore(sessionId)
|
||||
.onFailure {
|
||||
// Silently ignore this error (do not invoke onServiceUnregistered)
|
||||
Timber.tag(loggerTag.value).w(it, "Fails to restore client")
|
||||
}
|
||||
.flatMap { client ->
|
||||
client.rotateRegistration(clientSecret = clientSecret)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Issue during pusher unregistration / re registration")
|
||||
// Let the user know
|
||||
pushService.onServiceUnregistered(sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the pusher for the session. Then register again if possible.
|
||||
*/
|
||||
private suspend fun MatrixClient.rotateRegistration(clientSecret: String): Result<Unit> {
|
||||
val unregisterResult = unregisterUnifiedPushUseCase.unregister(
|
||||
matrixClient = this,
|
||||
clientSecret = clientSecret,
|
||||
unregisterUnifiedPush = false,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to unregister pusher")
|
||||
}
|
||||
return unregisterResult.flatMap {
|
||||
registerAgain()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to register again, if possible i.e. the current configuration is known and the
|
||||
* deletion of data in the UnifiedPush application has not already occurred in the last minute.
|
||||
*/
|
||||
private suspend fun MatrixClient.registerAgain(): Result<Unit> {
|
||||
return if (unifiedPushRemovedGatewayThrottler.canRegisterAgain()) {
|
||||
val pushProvider = pushService.getCurrentPushProvider(sessionId)
|
||||
val distributor = pushProvider?.getCurrentDistributor(sessionId)
|
||||
if (pushProvider != null && distributor != null) {
|
||||
pushService.registerWith(
|
||||
matrixClient = this,
|
||||
pushProvider = pushProvider,
|
||||
distributor = distributor,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).w(it, "Unable to register with current data")
|
||||
}
|
||||
} else {
|
||||
Result.failure(IllegalStateException("Unable to register again"))
|
||||
}
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).w("Second removal in less than 1 minute, do not register again")
|
||||
Result.failure(IllegalStateException("Too many requests to register again"))
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
|
||||
interface UnifiedPushSessionPushConfigProvider {
|
||||
suspend fun provide(sessionId: SessionId): Config?
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnifiedPushPushConfigProvider(
|
||||
private val pushClientSecret: PushClientSecret,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
) : UnifiedPushSessionPushConfigProvider {
|
||||
override suspend fun provide(sessionId: SessionId): Config? {
|
||||
val clientSecret = pushClientSecret.getSecretForUser(sessionId)
|
||||
val url = unifiedPushStore.getPushGateway(clientSecret) ?: return null
|
||||
val pushKey = unifiedPushStore.getEndpoint(clientSecret) ?: return null
|
||||
return Config(
|
||||
url = url,
|
||||
pushKey = pushKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
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 SharedPreferencesUnifiedPushStore(
|
||||
@ApplicationContext val context: Context,
|
||||
private val sharedPreferences: SharedPreferences,
|
||||
) : UnifiedPushStore {
|
||||
/**
|
||||
* Retrieves the UnifiedPush Endpoint.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the UnifiedPush Endpoint or null if not received
|
||||
*/
|
||||
override fun getEndpoint(clientSecret: String): String? {
|
||||
return sharedPreferences.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store UnifiedPush Endpoint to the SharedPrefs.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @param endpoint the endpoint to store
|
||||
*/
|
||||
override fun storeUpEndpoint(clientSecret: String, endpoint: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Push Gateway.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @return the Push Gateway or null if not defined
|
||||
*/
|
||||
override fun getPushGateway(clientSecret: String): String? {
|
||||
return sharedPreferences.getString(PREFS_PUSH_GATEWAY + clientSecret, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store Push Gateway to the SharedPrefs.
|
||||
*
|
||||
* @param clientSecret the client secret, to identify the session
|
||||
* @param gateway the push gateway to store
|
||||
*/
|
||||
override fun storePushGateway(clientSecret: String, gateway: String?) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_PUSH_GATEWAY + clientSecret, gateway)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDistributorValue(userId: UserId): String? {
|
||||
return sharedPreferences.getString(PREFS_DISTRIBUTOR + userId, null)
|
||||
}
|
||||
|
||||
override fun setDistributorValue(userId: UserId, value: String) {
|
||||
sharedPreferences.edit {
|
||||
putString(PREFS_DISTRIBUTOR + userId, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_ENDPOINT_OR_TOKEN = "UP_ENDPOINT_OR_TOKEN"
|
||||
private const val PREFS_PUSH_GATEWAY = "PUSH_GATEWAY"
|
||||
private const val PREFS_DISTRIBUTOR = "DISTRIBUTOR"
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.pushproviders.api.PusherSubscriber
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import timber.log.Timber
|
||||
|
||||
interface UnregisterUnifiedPushUseCase {
|
||||
/**
|
||||
* Unregister the app from the homeserver, then from UnifiedPush if [unregisterUnifiedPush] is true.
|
||||
*/
|
||||
suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
): Result<Unit>
|
||||
|
||||
/**
|
||||
* Cleanup any remaining data for the given client secret and unregister the app from UnifiedPush.
|
||||
*/
|
||||
fun cleanup(
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean = true,
|
||||
)
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUnregisterUnifiedPushUseCase(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val unifiedPushStore: UnifiedPushStore,
|
||||
private val pusherSubscriber: PusherSubscriber,
|
||||
) : UnregisterUnifiedPushUseCase {
|
||||
override suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
): Result<Unit> {
|
||||
val endpoint = unifiedPushStore.getEndpoint(clientSecret)
|
||||
val gateway = unifiedPushStore.getPushGateway(clientSecret)
|
||||
if (endpoint == null || gateway == null) {
|
||||
Timber.w("No endpoint or gateway found for client secret")
|
||||
// Ensure we don't have any remaining data, but ignore this error
|
||||
cleanup(clientSecret)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
return pusherSubscriber.unregisterPusher(matrixClient, endpoint, gateway)
|
||||
.onSuccess {
|
||||
cleanup(clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanup(clientSecret: String, unregisterUnifiedPush: Boolean) {
|
||||
unifiedPushStore.storeUpEndpoint(clientSecret, null)
|
||||
unifiedPushStore.storePushGateway(clientSecret, null)
|
||||
if (unregisterUnifiedPush) {
|
||||
UnifiedPush.unregister(context, clientSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
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.launch
|
||||
import org.unifiedpush.android.connector.FailedReason
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint
|
||||
import org.unifiedpush.android.connector.data.PushMessage
|
||||
import timber.log.Timber
|
||||
|
||||
private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver", LoggerTag.PushLoggerTag)
|
||||
|
||||
class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
|
||||
@Inject lateinit var pushParser: UnifiedPushParser
|
||||
@Inject lateinit var pushHandler: PushHandler
|
||||
@Inject lateinit var guardServiceStarter: GuardServiceStarter
|
||||
@Inject lateinit var unifiedPushStore: UnifiedPushStore
|
||||
@Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver
|
||||
@Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver
|
||||
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
|
||||
@Inject lateinit var removedGatewayHandler: UnifiedPushRemovedGatewayHandler
|
||||
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
|
||||
|
||||
@AppCoroutineScope
|
||||
@Inject lateinit var coroutineScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
context.bindings<VectorUnifiedPushMessagingReceiverBindings>().inject(this)
|
||||
super.onReceive(context, intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when message is received. The message contains the full POST body of the push message.
|
||||
*
|
||||
* @param context the Android context
|
||||
* @param message the message
|
||||
* @param instance connection, for multi-account
|
||||
*/
|
||||
override fun onMessage(context: Context, message: PushMessage, instance: String) {
|
||||
Timber.tag(loggerTag.value).d("New message, decrypted: ${message.decrypted}")
|
||||
coroutineScope.launch {
|
||||
val pushData = pushParser.parse(message.content, instance)
|
||||
if (pushData == null) {
|
||||
Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush")
|
||||
pushHandler.handleInvalid(
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
data = String(message.content),
|
||||
)
|
||||
} else {
|
||||
pushHandler.handle(
|
||||
pushData = pushData,
|
||||
providerInfo = "${UnifiedPushConfig.NAME} - $instance",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new endpoint is to be used for sending push messages.
|
||||
* You should send the endpoint to your application server and sync for missing notifications.
|
||||
*/
|
||||
override fun onNewEndpoint(context: Context, endpoint: PushEndpoint, instance: String) {
|
||||
Timber.tag(loggerTag.value).w("onNewEndpoint: $endpoint")
|
||||
coroutineScope.launch {
|
||||
val gateway = unifiedPushGatewayResolver.getGateway(endpoint.url)
|
||||
.let { gatewayResult ->
|
||||
unifiedPushGatewayUrlResolver.resolve(gatewayResult, instance)
|
||||
}
|
||||
unifiedPushStore.storePushGateway(instance, gateway)
|
||||
val result = newGatewayHandler.handle(endpoint.url, gateway, instance)
|
||||
.onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Failed to handle new gateway")
|
||||
}
|
||||
.onSuccess {
|
||||
unifiedPushStore.storeUpEndpoint(instance, endpoint.url)
|
||||
}
|
||||
endpointRegistrationHandler.registrationDone(
|
||||
RegistrationResult(
|
||||
clientSecret = instance,
|
||||
result = result,
|
||||
)
|
||||
)
|
||||
}
|
||||
guardServiceStarter.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the registration is not possible, eg. no network.
|
||||
*/
|
||||
override fun onRegistrationFailed(context: Context, reason: FailedReason, instance: String) {
|
||||
Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance, reason: $reason")
|
||||
coroutineScope.launch {
|
||||
endpointRegistrationHandler.registrationDone(
|
||||
RegistrationResult(
|
||||
clientSecret = instance,
|
||||
result = Result.failure(Exception("Registration failed. Reason: $reason")),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this application is unregistered from receiving push messages.
|
||||
*/
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
Timber.tag(loggerTag.value).w("onUnregistered $instance")
|
||||
coroutineScope.launch {
|
||||
removedGatewayHandler.handle(instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Binds
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
|
||||
@ContributesTo(AppScope::class)
|
||||
interface VectorUnifiedPushMessagingReceiverBindings {
|
||||
fun inject(receiver: VectorUnifiedPushMessagingReceiver)
|
||||
|
||||
@Binds
|
||||
fun bindsMessagingReceiver(vectorUnifiedPushMessagingReceiver: VectorUnifiedPushMessagingReceiver): MessagingReceiver
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DiscoveryResponse(
|
||||
@SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush()
|
||||
)
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DiscoveryUnifiedPush(
|
||||
@SerialName("gateway") val gateway: String = ""
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.network
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface UnifiedPushApi {
|
||||
@GET("_matrix/push/v1/notify")
|
||||
suspend fun discover(): DiscoveryResponse
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.registration
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
|
||||
data class RegistrationResult(
|
||||
val clientSecret: String,
|
||||
val result: Result<Unit>,
|
||||
)
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@Inject
|
||||
class EndpointRegistrationHandler {
|
||||
private val _state = MutableSharedFlow<RegistrationResult>()
|
||||
val state: SharedFlow<RegistrationResult> = _state
|
||||
|
||||
suspend fun registrationDone(result: RegistrationResult) {
|
||||
_state.emit(result)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import android.content.Context
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.system.openUrlInExternalApp
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
|
||||
|
||||
interface OpenDistributorWebPageAction {
|
||||
fun execute()
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultOpenDistributorWebPageAction(
|
||||
@ApplicationContext private val context: Context,
|
||||
) : OpenDistributorWebPageAction {
|
||||
override fun execute() {
|
||||
// Open the distributor download page
|
||||
context.openUrlInExternalApp(
|
||||
url = UnifiedPushConfig.UNIFIED_PUSH_DISTRIBUTORS_URL,
|
||||
)
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushApiFactory
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushSessionPushConfigProvider
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ContributesIntoSet(SessionScope::class)
|
||||
@Inject
|
||||
class UnifiedPushMatrixGatewayTest(
|
||||
private val sessionId: SessionId,
|
||||
private val unifiedPushApiFactory: UnifiedPushApiFactory,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val unifiedPushSessionPushConfigProvider: UnifiedPushSessionPushConfigProvider,
|
||||
) : NotificationTroubleshootTest {
|
||||
override val order = 450
|
||||
private val delegate = NotificationTroubleshootTestDelegate(
|
||||
defaultName = "Test push gateway",
|
||||
defaultDescription = "Ensure that the push gateway is valid.",
|
||||
visibleWhenIdle = false,
|
||||
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
|
||||
)
|
||||
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
|
||||
|
||||
override fun isRelevant(data: TestFilterData): Boolean {
|
||||
return data.currentPushProviderName == UnifiedPushConfig.NAME
|
||||
}
|
||||
|
||||
override suspend fun run(coroutineScope: CoroutineScope) {
|
||||
delegate.start()
|
||||
val config = unifiedPushSessionPushConfigProvider.provide(sessionId)
|
||||
if (config == null) {
|
||||
delegate.updateState(
|
||||
description = "No current push provider",
|
||||
status = NotificationTroubleshootTestState.Status.Failure()
|
||||
)
|
||||
} else {
|
||||
val gatewayBaseUrl = config.url.removeSuffix("/_matrix/push/v1/notify")
|
||||
// Checking if the gateway is a Matrix gateway
|
||||
coroutineScope.launch(coroutineDispatchers.io) {
|
||||
val api = unifiedPushApiFactory.create(gatewayBaseUrl)
|
||||
try {
|
||||
val discoveryResponse = api.discover()
|
||||
if (discoveryResponse.unifiedpush.gateway == "matrix") {
|
||||
delegate.updateState(
|
||||
description = "${config.url} is a Matrix gateway.",
|
||||
status = NotificationTroubleshootTestState.Status.Success
|
||||
)
|
||||
} else {
|
||||
delegate.updateState(
|
||||
description = "${config.url} is not a Matrix gateway.",
|
||||
status = NotificationTroubleshootTestState.Status.Failure()
|
||||
)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
delegate.updateState(
|
||||
description = "Fail to check the gateway ${config.url}: ${throwable.localizedMessage}",
|
||||
status = NotificationTroubleshootTestState.Status.Failure()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() = delegate.reset()
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesIntoSet
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.R
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootNavigator
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTest
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestDelegate
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@ContributesIntoSet(AppScope::class)
|
||||
@Inject
|
||||
class UnifiedPushTest(
|
||||
private val unifiedPushDistributorProvider: UnifiedPushDistributorProvider,
|
||||
private val openDistributorWebPageAction: OpenDistributorWebPageAction,
|
||||
private val stringProvider: StringProvider,
|
||||
) : NotificationTroubleshootTest {
|
||||
override val order = 400
|
||||
private val delegate = NotificationTroubleshootTestDelegate(
|
||||
defaultName = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_title),
|
||||
defaultDescription = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_description),
|
||||
visibleWhenIdle = false,
|
||||
fakeDelay = NotificationTroubleshootTestDelegate.SHORT_DELAY,
|
||||
)
|
||||
override val state: StateFlow<NotificationTroubleshootTestState> = delegate.state
|
||||
|
||||
override fun isRelevant(data: TestFilterData): Boolean {
|
||||
return data.currentPushProviderName == UnifiedPushConfig.NAME
|
||||
}
|
||||
|
||||
override suspend fun run(coroutineScope: CoroutineScope) {
|
||||
delegate.start()
|
||||
val distributors = unifiedPushDistributorProvider.getDistributors()
|
||||
if (distributors.isNotEmpty()) {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getQuantityString(
|
||||
resId = R.plurals.troubleshoot_notifications_test_unified_push_success,
|
||||
quantity = distributors.size,
|
||||
distributors.size,
|
||||
distributors.joinToString { it.name }
|
||||
),
|
||||
status = NotificationTroubleshootTestState.Status.Success
|
||||
)
|
||||
} else {
|
||||
delegate.updateState(
|
||||
description = stringProvider.getString(R.string.troubleshoot_notifications_test_unified_push_failure),
|
||||
status = NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() = delegate.reset()
|
||||
|
||||
override suspend fun quickFix(
|
||||
coroutineScope: CoroutineScope,
|
||||
navigator: NotificationTroubleshootNavigator,
|
||||
) {
|
||||
openDistributorWebPageAction.execute()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Пераканайцеся, што размеркавальнікі UnifiedPush даступныя."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Размеркавальнікі не знойдзены."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d знойдзены размеркавальнік: %2$s."</item>
|
||||
<item quantity="few">"%1$d знойдзены размеркавальнікі: %2$s."</item>
|
||||
<item quantity="many">"%1$d знойдзена размеркавальнікаў: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Праверыць UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Ujistěte se, že jsou k dispozici distributoři UnifiedPush."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nebyli nalezeni žádní push distributoři."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"Nalezen %1$d distributor: %2$s."</item>
|
||||
<item quantity="few">"Nalezeni %1$d distributoři: %2$s."</item>
|
||||
<item quantity="other">"Nalezeno %1$d distributorů: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Zkontrolovat UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Sicrhewch fod dosbarthwyr UnifiedPush ar gael."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Heb ganfod dosbarthwyr gwthio."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="zero">"Wedi canfod %1$d dosbarthwyr: %2$s"</item>
|
||||
<item quantity="one">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
|
||||
<item quantity="two">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
|
||||
<item quantity="few">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
|
||||
<item quantity="many">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
|
||||
<item quantity="other">"Wedi canfod %1$d dosbarthwr: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Gwiriwch UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Sørg for, at UnifiedPush-distributører er tilgængelige."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Ingen push-distributører fundet."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributør fundet:%2$s."</item>
|
||||
<item quantity="other">"%1$d distributører fundet:%2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Afprøv UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Stelle sicher, dass UnifiedPush-Verteiler verfügbar sind."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Keine Push-Verteiler gefunden."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d Verteiler gefunden: %2$s."</item>
|
||||
<item quantity="other">"%1$d Verteiler gefunden: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush prüfen"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Βεβαιώσου ότι οι διανομείς UnifiedPush είναι διαθέσιμοι."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Δεν βρέθηκαν διανομείς push."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%Βρέθηκε %1$d διανομέας: %2$s."</item>
|
||||
<item quantity="other">"Βρέθηκαν %1$d διανομείς: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Έλεγχος UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Asegurarse de que los distribuidores de UnifiedPush están disponibles."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"No se ha encontrado ningún distribuidor push."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distribuidor encontrado: %2$s."</item>
|
||||
<item quantity="other">"%1$d distribuidores encontrados: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Verificar UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Palun veendu, et UnifiedPushi levitajad on saadaval."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Tõuketeenuse levitajaid ei leidu."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"Leidus %1$d tõuketeenuse levitaja: %2$s."</item>
|
||||
<item quantity="other">"Leidus %1$d tõuketeenuse levitajat: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Kontrolli UnifiedPushi"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Ez da push banatzailerik aurkitu."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"Banatzaile %1$d aurkitu da: %2$s."</item>
|
||||
<item quantity="other">"%1$d banatzailea aurkitu dira: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Egiaztatu UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Varmistas, että UnifiedPush-jakelijat ovat käytettävissä."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Push-jakelijoita ei löytynyt."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d jakelija löytyi: %2$s."</item>
|
||||
<item quantity="other">"%1$d jakelijaa löytyi: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPushin tarkistus"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Vérifier qu’au moins un distributeur UnifiedPush est disponible."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Aucun distributeur UnifiedPush n’a été trouvé."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributeur détecté :%2$s."</item>
|
||||
<item quantity="other">"%1$d distributeurs détectés :%2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Vérifier UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Győződjön meg arról, hogy a UnifiedPush forgalmazói elérhetők."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nem található forgalmazó a leküldéses értesítésekhez."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d forgalmazó található: %2$s."</item>
|
||||
<item quantity="other">"%1$d forgalmazó található: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Ellenőrizze a UnifiedPush szolgáltatást"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Pastikan distributor UnifiedPush tersedia."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Tidak ada distributor notifikasi dorongan yang ditemukan."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="other">"%1$d distributor ditemukan: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Periksa UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Assicurati che i distributori UnifiedPush siano disponibili."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nessun distributore di notifiche push trovato."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributore trovato: %2$s."</item>
|
||||
<item quantity="other">"%1$d distributori trovati: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Controlla UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"დარწმუნდით რომ UnifiedPush დისტრიბუტორები ხელმისაწვდომია."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Push დისტრიბუტორები არ მოიძებნა."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d დისტრიბუტორი მოიძებნა: %2$s"</item>
|
||||
<item quantity="other">"%1$d დისტრიბუტორი მოიძებნა: %2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"შეამოწმეთ UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"UnifiedPush 배포자가 사용할 수 있는지 확인하세요."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"푸시 배포자가 발견되지 않았습니다."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="other">"%1$d 배포자 목록: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush 확인하기"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Påse at UnifiedPush-distributører er tilgjengelige."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Ingen push-distributører funnet."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributør funnet: %2$s."</item>
|
||||
<item quantity="other">"%1$d distributører funnet: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Sjekk UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Ervoor zorgen dat UnifiedPush verdelers beschikbaar zijn."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Geen push-verdelers gevonden."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d verdeler gevonden: %2$s."</item>
|
||||
<item quantity="other">"%1$d verdelers gevonden: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush controleren"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Upewnij się, że dystrybutorzy UnifiedPush są dostępni."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nie znaleziono dystrybutorów push."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"Znaleziono %1$d dystrybutora: %2$s."</item>
|
||||
<item quantity="few">"Znaleziono %1$d dystrybutorów: %2$s."</item>
|
||||
<item quantity="many">"Znaleziono %1$d dystrybutorów: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Sprawdź UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Certifique-se de que os distribuidores do UnifiedPush estejam disponíveis."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nenhum distribuidor push encontrado."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distribuidor encontrado: %2$s."</item>
|
||||
<item quantity="other">"%1$d distribuidores encontrados: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Verificar o UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Certifica que os distribuidores UnifiedPush estão disponíveis."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nenhum distribuidor encontrado."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distribuidor encontrado: %2$s."</item>
|
||||
<item quantity="other">"%1$d distribuidores encontrados: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Verificar UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Asigurați-vă că distribuitorii UnifiedPush sunt disponibili."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nu au fost găsiți distribuitori push."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distribuitor găsit: %2$s."</item>
|
||||
<item quantity="few">"%1$d distribuitori găsiți: %2$s."</item>
|
||||
<item quantity="other">"%1$d distribuitori găsiți: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Verificați UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Убедитесь, что дистрибьюторы UnifiedPush доступны."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Поставщиков push-уведомлений не найдено."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d провайдер найден: %2$s."</item>
|
||||
<item quantity="few">"%1$d провайдеров найдено: %2$s."</item>
|
||||
<item quantity="many">"%1$d провайдеров найдено: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Проверка UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Uistite sa, že sú dostupní distribútori UnifiedPush."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Nenašli sa žiadni distribútori push."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d nájdený distribútor: %2$s."</item>
|
||||
<item quantity="few">"%1$d nájdení distribútori: %2$s."</item>
|
||||
<item quantity="other">"%1$d nájdených distribútorov: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Skontrolovať UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Se till att UnifiedPush-distributörer är tillgängliga."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Inga push-distributörer hittades."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributör hittades:%2$s."</item>
|
||||
<item quantity="other">"%1$d distributörer hittade:%2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Kontrollera UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"UnifiedPush distribütörlerinin mevcut olduğundan emin olun."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"İtme dağıtıcı bulunamadı."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d dağıtıcı bulundu: %2$s."</item>
|
||||
<item quantity="other">"%1$d dağıtıcı bulundu: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush\'u kontrol edin"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Переконується, що дистриб\'ютори UnifiedPush доступні."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Дистриб\'юторів не знайдено."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"Знайдений %1$d дистриб\'ютор: %2$s."</item>
|
||||
<item quantity="few">"Знайдено %1$d дистриб\'ютори: %2$s."</item>
|
||||
<item quantity="many">"Знайдено %1$d дистриб\'юторів: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Перевірка UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"یقینی بنائیں کہ UnifiedPush تقسیم کاران دستیاب ہیں۔"</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"کوئی دھکا تقسیم کاران نہیں ملے۔"</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d تقسیم کار ملا: %2$s۔"</item>
|
||||
<item quantity="other">"%1$d تقسیم کاران ملے: %2$s۔"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush کی پڑتال کریں"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"UnifiedPush distribyutorlari mavjudligiga ishonch hosil qiling."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"Push distribyutorlari topilmadi."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d ta distribyutor topildi: %2$s."</item>
|
||||
<item quantity="other">"%1$d ta distribyutor topildi: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"UnifiedPush tekshiruvi"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"確保 UnifiedPush 散佈者可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"找不到散佈者。"</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="other">"找到 %1$d 個散佈者:%2$s。"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"檢查 UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"确保 UnifiedPush distributor 可用。"</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"未找到推送 distributor。"</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="other">"找到 %1$d 个 distributors:%2$s"</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"检查 UnifiedPush"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="troubleshoot_notifications_test_unified_push_description">"Ensure that UnifiedPush distributors are available."</string>
|
||||
<string name="troubleshoot_notifications_test_unified_push_failure">"No push distributors found."</string>
|
||||
<plurals name="troubleshoot_notifications_test_unified_push_success">
|
||||
<item quantity="one">"%1$d distributor found: %2$s."</item>
|
||||
<item quantity="other">"%1$d distributors found: %2$s."</item>
|
||||
</plurals>
|
||||
<string name="troubleshoot_notifications_test_unified_push_title">"Check UnifiedPush"</string>
|
||||
</resources>
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.test.A_SECRET
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultUnifiedPushCurrentUserPushConfigProviderTest {
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig no push gateway`() = runTest {
|
||||
val sut = createDefaultUnifiedPushCurrentUserPushConfigProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = { A_SECRET }
|
||||
),
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getPushGatewayResult = { null }
|
||||
),
|
||||
)
|
||||
val result = sut.provide(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig no push key`() = runTest {
|
||||
val sut = createDefaultUnifiedPushCurrentUserPushConfigProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = { A_SECRET }
|
||||
),
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getPushGatewayResult = { "aPushGateway" },
|
||||
getEndpointResult = { null }
|
||||
),
|
||||
)
|
||||
val result = sut.provide(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig ok`() = runTest {
|
||||
val sut = createDefaultUnifiedPushCurrentUserPushConfigProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = { A_SECRET }
|
||||
),
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getPushGatewayResult = { "aPushGateway" },
|
||||
getEndpointResult = { "aEndpoint" }
|
||||
),
|
||||
)
|
||||
val result = sut.provide(A_SESSION_ID)
|
||||
assertThat(result).isEqualTo(Config("aPushGateway", "aEndpoint"))
|
||||
}
|
||||
|
||||
private fun createDefaultUnifiedPushCurrentUserPushConfigProvider(
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||
): DefaultUnifiedPushPushConfigProvider {
|
||||
return DefaultUnifiedPushPushConfigProvider(
|
||||
pushClientSecret = pushClientSecret,
|
||||
unifiedPushStore = unifiedPushStore,
|
||||
)
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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 okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.Test
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.net.HttpURLConnection
|
||||
|
||||
internal val matrixDiscoveryResponse = {
|
||||
DiscoveryResponse(
|
||||
unifiedpush = DiscoveryUnifiedPush(
|
||||
gateway = "matrix"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
internal val invalidDiscoveryResponse = {
|
||||
DiscoveryResponse(
|
||||
unifiedpush = DiscoveryUnifiedPush(
|
||||
gateway = ""
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class DefaultUnifiedPushGatewayResolverTest {
|
||||
@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(UnifiedPushGatewayResolverResult.Success("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(UnifiedPushGatewayResolverResult.Success("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(UnifiedPushGatewayResolverResult.Success("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(UnifiedPushGatewayResolverResult.Success("http://custom.url:123/_matrix/push/v1/notify"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is not reachable, the custom url is still 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(UnifiedPushGatewayResolverResult.Error("http://custom.url/_matrix/push/v1/notify"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is not found (404), NoMatrixGateway is returned`() = runTest {
|
||||
val unifiedPushApiFactory = FakeUnifiedPushApiFactory(
|
||||
discoveryResponse = {
|
||||
throw HttpException(Response.error<Unit>(HttpURLConnection.HTTP_NOT_FOUND, "".toResponseBody()))
|
||||
}
|
||||
)
|
||||
val sut = createDefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory = unifiedPushApiFactory
|
||||
)
|
||||
val result = sut.getGateway("http://custom.url")
|
||||
assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url")
|
||||
assertThat(result).isEqualTo(UnifiedPushGatewayResolverResult.NoMatrixGateway)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is forbidden (403), NoMatrixGateway is returned`() = runTest {
|
||||
val unifiedPushApiFactory = FakeUnifiedPushApiFactory(
|
||||
discoveryResponse = {
|
||||
throw HttpException(Response.error<Unit>(HttpURLConnection.HTTP_FORBIDDEN, "".toResponseBody()))
|
||||
}
|
||||
)
|
||||
val sut = createDefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory = unifiedPushApiFactory
|
||||
)
|
||||
val result = sut.getGateway("http://custom.url")
|
||||
assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url")
|
||||
assertThat(result).isEqualTo(UnifiedPushGatewayResolverResult.NoMatrixGateway)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is not acceptable (406), NoMatrixGateway is returned`() = runTest {
|
||||
val unifiedPushApiFactory = FakeUnifiedPushApiFactory(
|
||||
discoveryResponse = {
|
||||
throw HttpException(Response.error<Unit>(HttpURLConnection.HTTP_NOT_ACCEPTABLE, "".toResponseBody()))
|
||||
}
|
||||
)
|
||||
val sut = createDefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory = unifiedPushApiFactory
|
||||
)
|
||||
val result = sut.getGateway("http://custom.url")
|
||||
assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url")
|
||||
assertThat(result).isEqualTo(UnifiedPushGatewayResolverResult.NoMatrixGateway)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is internal error (500), Error is returned`() = runTest {
|
||||
val unifiedPushApiFactory = FakeUnifiedPushApiFactory(
|
||||
discoveryResponse = {
|
||||
throw HttpException(Response.error<Unit>(HttpURLConnection.HTTP_INTERNAL_ERROR, "".toResponseBody()))
|
||||
}
|
||||
)
|
||||
val sut = createDefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory = unifiedPushApiFactory
|
||||
)
|
||||
val result = sut.getGateway("http://custom.url")
|
||||
assertThat(unifiedPushApiFactory.baseUrlParameter).isEqualTo("http://custom.url")
|
||||
assertThat(result).isEqualTo(UnifiedPushGatewayResolverResult.Error("http://custom.url/_matrix/push/v1/notify"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url is invalid, ErrorInvalidUrl 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(UnifiedPushGatewayResolverResult.ErrorInvalidUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when a custom url provides a invalid matrix gateway, NoMatrixGateway 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(UnifiedPushGatewayResolverResult.NoMatrixGateway)
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory: UnifiedPushApiFactory = FakeUnifiedPushApiFactory(
|
||||
discoveryResponse = { DiscoveryResponse() }
|
||||
)
|
||||
) = DefaultUnifiedPushGatewayResolver(
|
||||
unifiedPushApiFactory = unifiedPushApiFactory,
|
||||
coroutineDispatchers = testCoroutineDispatchers()
|
||||
)
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultUnifiedPushGatewayUrlResolverTest {
|
||||
@Test
|
||||
fun `resolve ErrorInvalidUrl returns the default gateway`() {
|
||||
val sut = createDefaultUnifiedPushGatewayUrlResolver()
|
||||
val result = sut.resolve(
|
||||
gatewayResult = UnifiedPushGatewayResolverResult.ErrorInvalidUrl,
|
||||
instance = "",
|
||||
)
|
||||
assertThat(result).isEqualTo(A_UNIFIED_PUSH_GATEWAY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve NoMatrixGateway returns the default gateway`() {
|
||||
val sut = createDefaultUnifiedPushGatewayUrlResolver()
|
||||
val result = sut.resolve(
|
||||
gatewayResult = UnifiedPushGatewayResolverResult.NoMatrixGateway,
|
||||
instance = "",
|
||||
)
|
||||
assertThat(result).isEqualTo(A_UNIFIED_PUSH_GATEWAY)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve Success returns the url`() {
|
||||
val sut = createDefaultUnifiedPushGatewayUrlResolver()
|
||||
val result = sut.resolve(
|
||||
gatewayResult = UnifiedPushGatewayResolverResult.Success("aUrl"),
|
||||
instance = "",
|
||||
)
|
||||
assertThat(result).isEqualTo("aUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve Error returns the current url when available`() {
|
||||
val sut = createDefaultUnifiedPushGatewayUrlResolver(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getPushGatewayResult = { instance ->
|
||||
assertThat(instance).isEqualTo("instance")
|
||||
"aCurrentUrl"
|
||||
},
|
||||
)
|
||||
)
|
||||
val result = sut.resolve(
|
||||
gatewayResult = UnifiedPushGatewayResolverResult.Error("aUrl"),
|
||||
instance = "instance",
|
||||
)
|
||||
assertThat(result).isEqualTo("aCurrentUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolve Error returns the url if no current url is available`() {
|
||||
val sut = createDefaultUnifiedPushGatewayUrlResolver(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getPushGatewayResult = { instance ->
|
||||
assertThat(instance).isEqualTo("instance")
|
||||
null
|
||||
},
|
||||
)
|
||||
)
|
||||
val result = sut.resolve(
|
||||
gatewayResult = UnifiedPushGatewayResolverResult.Error("aUrl"),
|
||||
instance = "instance",
|
||||
)
|
||||
assertThat(result).isEqualTo("aUrl")
|
||||
}
|
||||
|
||||
private fun createDefaultUnifiedPushGatewayUrlResolver(
|
||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||
defaultPushGatewayHttpUrlProvider: DefaultPushGatewayHttpUrlProvider = FakeDefaultPushGatewayHttpUrlProvider(),
|
||||
) = DefaultUnifiedPushGatewayUrlResolver(
|
||||
unifiedPushStore = unifiedPushStore,
|
||||
defaultPushGatewayHttpUrlProvider = defaultPushGatewayHttpUrlProvider,
|
||||
)
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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.MatrixClientProvider
|
||||
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.FakeMatrixClientProvider
|
||||
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")) }
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider { 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
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider { 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(),
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider()
|
||||
): DefaultUnifiedPushNewGatewayHandler {
|
||||
return DefaultUnifiedPushNewGatewayHandler(
|
||||
pusherSubscriber = pusherSubscriber,
|
||||
userPushStoreFactory = userPushStoreFactory,
|
||||
pushClientSecret = pushClientSecret,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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.MatrixClientProvider
|
||||
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.matrix.test.FakeMatrixClientProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.test.FakePushService
|
||||
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.clientsecret.PushClientSecret
|
||||
import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret
|
||||
import io.element.android.tests.testutils.lambda.any
|
||||
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.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class DefaultUnifiedPushRemovedGatewayHandlerTest {
|
||||
@Test
|
||||
fun `handle returns error if the secret is unknown`() = runTest {
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { null },
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot restore the client`() = runTest {
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot unregister the pusher, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot get current push provider, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = { null },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot get current distributor, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { null },
|
||||
)
|
||||
},
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns error if cannot register again, and user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = { _, _, _ -> Result.success(Unit) },
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = { _, _, _ -> Result.failure(AN_EXCEPTION) },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isFailure).isTrue()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns success if can register again, and user is not notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = { _, _, _ -> Result.success(Unit) },
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handle returns success if can register again, but after 2 removals user is notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val registerWithLambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = registerWithLambda,
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
// Second attempt in less than 1 minute
|
||||
val result2 = sut.handle(A_SECRET)
|
||||
assertThat(result2.isFailure).isTrue()
|
||||
unregisterLambda.assertions().isCalledExactly(2)
|
||||
// Registration is not called twice
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isCalledOnce().with(value(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `handle returns success if can register again, but after 2 distant removals user is not notified`() = runTest {
|
||||
val onServiceUnregisteredResult = lambdaRecorder<UserId, Unit> { }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val registerWithLambda = lambdaRecorder<MatrixClient, PushProvider, Distributor, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val sut = createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getUserIdFromSecretResult = { A_SESSION_ID },
|
||||
),
|
||||
matrixClientProvider = FakeMatrixClientProvider(
|
||||
getClient = { Result.success(FakeMatrixClient()) },
|
||||
),
|
||||
unregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
pushService = FakePushService(
|
||||
currentPushProvider = {
|
||||
FakePushProvider(
|
||||
currentDistributor = { Distributor("aValue", "aName") },
|
||||
)
|
||||
},
|
||||
registerWithLambda = registerWithLambda,
|
||||
onServiceUnregisteredResult = onServiceUnregisteredResult,
|
||||
),
|
||||
)
|
||||
val result = sut.handle(A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledOnce().with(
|
||||
any(),
|
||||
value(A_SECRET),
|
||||
value(false),
|
||||
)
|
||||
registerWithLambda.assertions().isCalledOnce()
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
// Second attempt in more than 1 minute
|
||||
advanceTimeBy(61.seconds)
|
||||
val result2 = sut.handle(A_SECRET)
|
||||
assertThat(result2.isSuccess).isTrue()
|
||||
unregisterLambda.assertions().isCalledExactly(2)
|
||||
// Registration is not called twice
|
||||
registerWithLambda.assertions().isCalledExactly(2)
|
||||
onServiceUnregisteredResult.assertions().isNeverCalled()
|
||||
}
|
||||
|
||||
private fun TestScope.createDefaultUnifiedPushRemovedGatewayHandler(
|
||||
unregisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
matrixClientProvider: MatrixClientProvider = FakeMatrixClientProvider(),
|
||||
pushService: PushService = FakePushService(),
|
||||
) = DefaultUnifiedPushRemovedGatewayHandler(
|
||||
unregisterUnifiedPushUseCase = unregisterUnifiedPushUseCase,
|
||||
pushClientSecret = pushClientSecret,
|
||||
matrixClientProvider = matrixClientProvider,
|
||||
pushService = pushService,
|
||||
unifiedPushRemovedGatewayThrottler = UnifiedPushRemovedGatewayThrottler(
|
||||
appCoroutineScope = backgroundScope,
|
||||
),
|
||||
)
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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.unregister(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 - will not unregister but return success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val storeUpEndpointResult = lambdaRecorder { _: String, _: String? -> }
|
||||
val storePushGatewayResult = lambdaRecorder { _: String, _: String? -> }
|
||||
val useCase = createDefaultUnregisterUnifiedPushUseCase(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getEndpointResult = { null },
|
||||
getPushGatewayResult = { "aGateway" },
|
||||
storeUpEndpointResult = storeUpEndpointResult,
|
||||
storePushGatewayResult = storePushGatewayResult,
|
||||
),
|
||||
)
|
||||
val result = useCase.unregister(matrixClient, A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
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 gateway - will not unregister but return success`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val storeUpEndpointResult = lambdaRecorder { _: String, _: String? -> }
|
||||
val storePushGatewayResult = lambdaRecorder { _: String, _: String? -> }
|
||||
val useCase = createDefaultUnregisterUnifiedPushUseCase(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getEndpointResult = { "aEndpoint" },
|
||||
getPushGatewayResult = { null },
|
||||
storeUpEndpointResult = storeUpEndpointResult,
|
||||
storePushGatewayResult = storePushGatewayResult,
|
||||
),
|
||||
)
|
||||
val result = useCase.unregister(matrixClient, A_SECRET)
|
||||
assertThat(result.isSuccess).isTrue()
|
||||
storeUpEndpointResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SECRET), value(null))
|
||||
storePushGatewayResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SECRET), value(null))
|
||||
}
|
||||
|
||||
@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.unregister(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
|
||||
)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
const val A_UNIFIED_PUSH_GATEWAY = "aGateway"
|
||||
|
||||
class FakeDefaultPushGatewayHttpUrlProvider(
|
||||
private val provideResult: () -> String = { A_UNIFIED_PUSH_GATEWAY }
|
||||
) : DefaultPushGatewayHttpUrlProvider {
|
||||
override fun provide(): String {
|
||||
return provideResult()
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnifiedPushGatewayResolver(
|
||||
private val getGatewayResult: (String) -> UnifiedPushGatewayResolverResult = { lambdaError() },
|
||||
) : UnifiedPushGatewayResolver {
|
||||
override suspend fun getGateway(endpoint: String): UnifiedPushGatewayResolverResult {
|
||||
return getGatewayResult(endpoint)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnifiedPushGatewayUrlResolver(
|
||||
private val resolveResult: (UnifiedPushGatewayResolverResult, String) -> String = { _, _ -> lambdaError() },
|
||||
) : UnifiedPushGatewayUrlResolver {
|
||||
override fun resolve(gatewayResult: UnifiedPushGatewayResolverResult, instance: String): String {
|
||||
return resolveResult(gatewayResult, instance)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnifiedPushNewGatewayHandler(
|
||||
private val handleResult: (String, String, String) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
) : UnifiedPushNewGatewayHandler {
|
||||
override suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String): Result<Unit> {
|
||||
return handleResult(endpoint, pushGateway, clientSecret)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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 unregisterLambda: (MatrixClient, String, Boolean) -> Result<Unit> = { _, _, _ -> lambdaError() },
|
||||
private val cleanupLambda: (String, Boolean) -> Unit = { _, _ -> lambdaError() },
|
||||
) : UnregisterUnifiedPushUseCase {
|
||||
override suspend fun unregister(
|
||||
matrixClient: MatrixClient,
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
): Result<Unit> {
|
||||
return unregisterLambda(matrixClient, clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
|
||||
override fun cleanup(
|
||||
clientSecret: String,
|
||||
unregisterUnifiedPush: Boolean,
|
||||
) {
|
||||
cleanupLambda(clientSecret, unregisterUnifiedPush)
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
||||
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.pushproviders.api.PushData
|
||||
import io.element.android.tests.testutils.assertThrowsInDebug
|
||||
import org.junit.Test
|
||||
|
||||
class UnifiedPushParserTest {
|
||||
private val aClientSecret = "a-client-secret"
|
||||
private val validData = PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 1,
|
||||
clientSecret = aClientSecret
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `test edge cases UnifiedPush`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
// Empty string
|
||||
assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull()
|
||||
// Empty Json
|
||||
assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull()
|
||||
// Bad Json
|
||||
assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPush format`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty roomId`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test invalid roomId`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test empty eventId`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test invalid eventId`() {
|
||||
val pushParser = createUnifiedPushParser()
|
||||
assertThrowsInDebug {
|
||||
pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val UNIFIED_PUSH_DATA =
|
||||
"{\"notification\":{\"event_id\":\"$AN_EVENT_ID\",\"room_id\":\"$A_ROOM_ID\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.mutate(oldValue: String, newValue: String): ByteArray {
|
||||
return replace(oldValue, newValue).toByteArray()
|
||||
}
|
||||
|
||||
fun createUnifiedPushParser() = UnifiedPushParser(
|
||||
json = DefaultJsonProvider(),
|
||||
)
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
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.Distributor
|
||||
import io.element.android.libraries.pushproviders.test.aSessionPushConfig
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.troubleshoot.FakeUnifiedPushDistributorProvider
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.troubleshoot.FakeUnifiedPushSessionPushConfigProvider
|
||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||
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 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"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDistributors return empty`() {
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
|
||||
getDistributorsResult = emptyList()
|
||||
)
|
||||
)
|
||||
val result = unifiedPushProvider.getDistributors()
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@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 unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.success(Unit) }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = getSecretForUserResultLambda,
|
||||
),
|
||||
unRegisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
)
|
||||
val result = unifiedPushProvider.unregister(matrixClient)
|
||||
assertThat(result).isEqualTo(Result.success(Unit))
|
||||
getSecretForUserResultLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID))
|
||||
unregisterLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregister ko`() = runTest {
|
||||
val matrixClient = FakeMatrixClient()
|
||||
val getSecretForUserResultLambda = lambdaRecorder<SessionId, String> { A_SECRET }
|
||||
val unregisterLambda = lambdaRecorder<MatrixClient, String, Boolean, Result<Unit>> { _, _, _ -> Result.failure(AN_EXCEPTION) }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = getSecretForUserResultLambda,
|
||||
),
|
||||
unRegisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
unregisterLambda = unregisterLambda,
|
||||
),
|
||||
)
|
||||
val result = unifiedPushProvider.unregister(matrixClient)
|
||||
assertThat(result).isEqualTo(Result.failure<Unit>(AN_EXCEPTION))
|
||||
getSecretForUserResultLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SESSION_ID))
|
||||
unregisterLambda.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(matrixClient), value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentDistributor ok`() = runTest {
|
||||
val distributor = Distributor("value", "Name")
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getDistributorValueResult = { distributor.value }
|
||||
),
|
||||
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
|
||||
getDistributorsResult = listOf(
|
||||
Distributor("value2", "Name2"),
|
||||
distributor,
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = unifiedPushProvider.getCurrentDistributor(A_SESSION_ID)
|
||||
assertThat(result).isEqualTo(distributor)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentDistributor not know`() = runTest {
|
||||
val distributor = Distributor("value", "Name")
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getDistributorValueResult = { "unknown" }
|
||||
),
|
||||
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
|
||||
getDistributorsResult = listOf(
|
||||
distributor,
|
||||
)
|
||||
)
|
||||
)
|
||||
val result = unifiedPushProvider.getCurrentDistributor(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentDistributor not found`() = runTest {
|
||||
val distributor = Distributor("value", "Name")
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
unifiedPushStore = FakeUnifiedPushStore(
|
||||
getDistributorValueResult = { distributor.value }
|
||||
),
|
||||
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
|
||||
getDistributorsResult = emptyList()
|
||||
)
|
||||
)
|
||||
val result = unifiedPushProvider.getCurrentDistributor(A_SESSION_ID)
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getCurrentUserPushConfig invokes the provider methods`() = runTest {
|
||||
val currentUserPushConfig = aSessionPushConfig()
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
unifiedPushSessionPushConfigProvider = FakeUnifiedPushSessionPushConfigProvider(
|
||||
config = { currentUserPushConfig }
|
||||
)
|
||||
)
|
||||
val result = unifiedPushProvider.getPushConfig(A_SESSION_ID)
|
||||
assertThat(result).isEqualTo(currentUserPushConfig)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `canRotateToken should return false`() = runTest {
|
||||
val unifiedPushProvider = createUnifiedPushProvider()
|
||||
assertThat(unifiedPushProvider.canRotateToken()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onSessionDeleted should do the cleanup`() = runTest {
|
||||
val cleanupLambda = lambdaRecorder<String, Boolean, Unit> { _, _ -> }
|
||||
val unifiedPushProvider = createUnifiedPushProvider(
|
||||
pushClientSecret = FakePushClientSecret(
|
||||
getSecretForUserResult = { A_SECRET }
|
||||
),
|
||||
unRegisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(
|
||||
cleanupLambda = cleanupLambda,
|
||||
),
|
||||
)
|
||||
unifiedPushProvider.onSessionDeleted(A_SESSION_ID)
|
||||
cleanupLambda.assertions().isCalledOnce().with(value(A_SECRET), value(true))
|
||||
}
|
||||
|
||||
private fun createUnifiedPushProvider(
|
||||
unifiedPushDistributorProvider: UnifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(),
|
||||
registerUnifiedPushUseCase: RegisterUnifiedPushUseCase = FakeRegisterUnifiedPushUseCase(),
|
||||
unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase = FakeUnregisterUnifiedPushUseCase(),
|
||||
pushClientSecret: PushClientSecret = FakePushClientSecret(),
|
||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||
unifiedPushSessionPushConfigProvider: UnifiedPushSessionPushConfigProvider = FakeUnifiedPushSessionPushConfigProvider(),
|
||||
): UnifiedPushProvider {
|
||||
return UnifiedPushProvider(
|
||||
unifiedPushDistributorProvider = unifiedPushDistributorProvider,
|
||||
registerUnifiedPushUseCase = registerUnifiedPushUseCase,
|
||||
unRegisterUnifiedPushUseCase = unRegisterUnifiedPushUseCase,
|
||||
pushClientSecret = pushClientSecret,
|
||||
unifiedPushStore = unifiedPushStore,
|
||||
unifiedPushSessionPushConfigProvider = unifiedPushSessionPushConfigProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush
|
||||
|
||||
import android.content.Intent
|
||||
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.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.TestScope
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.unifiedpush.android.connector.FailedReason
|
||||
import org.unifiedpush.android.connector.data.PublicKeySet
|
||||
import org.unifiedpush.android.connector.data.PushEndpoint
|
||||
import org.unifiedpush.android.connector.data.PushMessage
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class VectorUnifiedPushMessagingReceiverTest {
|
||||
@Test
|
||||
fun `onReceive does the binding`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
|
||||
// The binding is not found in the test env.
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
vectorUnifiedPushMessagingReceiver.onReceive(context, Intent())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onUnregistered invokes the removedGatewayHandler`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val handleResult = lambdaRecorder<String, Result<Unit>> {
|
||||
Result.success(Unit)
|
||||
}
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
removedGatewayHandler = UnifiedPushRemovedGatewayHandler { handleResult(it) },
|
||||
)
|
||||
vectorUnifiedPushMessagingReceiver.onUnregistered(context, A_SECRET)
|
||||
advanceUntilIdle()
|
||||
handleResult.assertions().isCalledOnce().with(value(A_SECRET))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onRegistrationFailed does nothing`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver()
|
||||
vectorUnifiedPushMessagingReceiver.onRegistrationFailed(context, FailedReason.NETWORK, A_SECRET)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessage valid invokes the push handler`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val pushHandlerResult = lambdaRecorder<PushData, String, Unit> { _, _ -> }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleResult = pushHandlerResult
|
||||
),
|
||||
)
|
||||
vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(), A_SECRET)
|
||||
advanceUntilIdle()
|
||||
pushHandlerResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(
|
||||
value(
|
||||
PushData(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
unread = 1,
|
||||
clientSecret = A_SECRET
|
||||
)
|
||||
),
|
||||
value(
|
||||
UnifiedPushConfig.NAME + " - " + A_SECRET
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onMessage invalid invokes the push handler invalid method`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val handleInvalidResult = lambdaRecorder<String, String, Unit> { _, _ -> }
|
||||
val vectorUnifiedPushMessagingReceiver = createVectorUnifiedPushMessagingReceiver(
|
||||
pushHandler = FakePushHandler(
|
||||
handleInvalidResult = handleInvalidResult,
|
||||
),
|
||||
)
|
||||
vectorUnifiedPushMessagingReceiver.onMessage(context, aPushMessage(""), A_SECRET)
|
||||
advanceUntilIdle()
|
||||
handleInvalidResult.assertions().isCalledOnce()
|
||||
}
|
||||
|
||||
@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 = { UnifiedPushGatewayResolverResult.Success("aGateway") }
|
||||
),
|
||||
unifiedPushGatewayUrlResolver = FakeUnifiedPushGatewayUrlResolver(
|
||||
resolveResult = { _, _ -> "aGatewayUrl" }
|
||||
),
|
||||
endpointRegistrationHandler = endpointRegistrationHandler,
|
||||
unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler,
|
||||
)
|
||||
endpointRegistrationHandler.state.test {
|
||||
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint("anEndpoint"), A_SECRET)
|
||||
advanceUntilIdle()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RegistrationResult(
|
||||
clientSecret = A_SECRET,
|
||||
result = Result.success(Unit)
|
||||
)
|
||||
)
|
||||
}
|
||||
storePushGatewayResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SECRET), value("aGatewayUrl"))
|
||||
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 = { UnifiedPushGatewayResolverResult.Success("aGateway") }
|
||||
),
|
||||
unifiedPushGatewayUrlResolver = FakeUnifiedPushGatewayUrlResolver(
|
||||
resolveResult = { _, _ -> "aGatewayUrl" }
|
||||
),
|
||||
endpointRegistrationHandler = endpointRegistrationHandler,
|
||||
unifiedPushNewGatewayHandler = unifiedPushNewGatewayHandler,
|
||||
)
|
||||
endpointRegistrationHandler.state.test {
|
||||
vectorUnifiedPushMessagingReceiver.onNewEndpoint(context, aPushEndpoint(), A_SECRET)
|
||||
advanceUntilIdle()
|
||||
assertThat(awaitItem()).isEqualTo(
|
||||
RegistrationResult(
|
||||
clientSecret = A_SECRET,
|
||||
result = Result.failure(AN_EXCEPTION)
|
||||
)
|
||||
)
|
||||
}
|
||||
storePushGatewayResult.assertions()
|
||||
.isCalledOnce()
|
||||
.with(value(A_SECRET), value("aGatewayUrl"))
|
||||
storeUpEndpointResult.assertions()
|
||||
.isNeverCalled()
|
||||
}
|
||||
|
||||
private fun TestScope.createVectorUnifiedPushMessagingReceiver(
|
||||
unifiedPushParser: UnifiedPushParser = createUnifiedPushParser(),
|
||||
pushHandler: PushHandler = FakePushHandler(),
|
||||
unifiedPushStore: UnifiedPushStore = FakeUnifiedPushStore(),
|
||||
unifiedPushGatewayResolver: UnifiedPushGatewayResolver = FakeUnifiedPushGatewayResolver(),
|
||||
unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver = FakeUnifiedPushGatewayUrlResolver(),
|
||||
unifiedPushNewGatewayHandler: UnifiedPushNewGatewayHandler = FakeUnifiedPushNewGatewayHandler(),
|
||||
endpointRegistrationHandler: EndpointRegistrationHandler = EndpointRegistrationHandler(),
|
||||
removedGatewayHandler: UnifiedPushRemovedGatewayHandler = UnifiedPushRemovedGatewayHandler { lambdaError() },
|
||||
): VectorUnifiedPushMessagingReceiver {
|
||||
return VectorUnifiedPushMessagingReceiver().apply {
|
||||
this.pushParser = unifiedPushParser
|
||||
this.pushHandler = pushHandler
|
||||
this.guardServiceStarter = NoopGuardServiceStarter()
|
||||
this.unifiedPushStore = unifiedPushStore
|
||||
this.unifiedPushGatewayResolver = unifiedPushGatewayResolver
|
||||
this.unifiedPushGatewayUrlResolver = unifiedPushGatewayUrlResolver
|
||||
this.newGatewayHandler = unifiedPushNewGatewayHandler
|
||||
this.removedGatewayHandler = removedGatewayHandler
|
||||
this.endpointRegistrationHandler = endpointRegistrationHandler
|
||||
this.coroutineScope = this@createVectorUnifiedPushMessagingReceiver
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun aPushMessage(
|
||||
data: String = UnifiedPushParserTest.UNIFIED_PUSH_DATA,
|
||||
decrypted: Boolean = true,
|
||||
) = PushMessage(
|
||||
content = data.toByteArray(),
|
||||
decrypted = decrypted,
|
||||
)
|
||||
|
||||
private fun aPushEndpoint(
|
||||
url: String = "anEndpoint",
|
||||
pubKeySet: PublicKeySet? = null,
|
||||
temporary: Boolean = false,
|
||||
) = PushEndpoint(
|
||||
url = url,
|
||||
pubKeySet = pubKeySet,
|
||||
temporary = temporary,
|
||||
)
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
class FakeOpenDistributorWebPageAction(
|
||||
private val executeAction: () -> Unit = {}
|
||||
) : OpenDistributorWebPageAction {
|
||||
override fun execute() = executeAction()
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushDistributorProvider
|
||||
|
||||
class FakeUnifiedPushDistributorProvider(
|
||||
private var getDistributorsResult: List<Distributor> = emptyList()
|
||||
) : UnifiedPushDistributorProvider {
|
||||
override fun getDistributors(): List<Distributor> {
|
||||
return getDistributorsResult
|
||||
}
|
||||
|
||||
fun setDistributorsResult(list: List<Distributor>) {
|
||||
getDistributorsResult = list
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushSessionPushConfigProvider
|
||||
import io.element.android.tests.testutils.lambda.lambdaError
|
||||
|
||||
class FakeUnifiedPushSessionPushConfigProvider(
|
||||
private val config: (SessionId) -> Config? = { lambdaError() },
|
||||
) : UnifiedPushSessionPushConfigProvider {
|
||||
override suspend fun provide(sessionId: SessionId): Config? {
|
||||
return config(sessionId)
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.pushproviders.api.Config
|
||||
import io.element.android.libraries.pushproviders.test.aSessionPushConfig
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.FakeUnifiedPushApiFactory
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.UnifiedPushConfig
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.invalidDiscoveryResponse
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.matrixDiscoveryResponse
|
||||
import io.element.android.libraries.pushproviders.unifiedpush.network.DiscoveryResponse
|
||||
import io.element.android.libraries.troubleshoot.api.test.NotificationTroubleshootTestState
|
||||
import io.element.android.libraries.troubleshoot.api.test.TestFilterData
|
||||
import io.element.android.libraries.troubleshoot.test.runAndTestState
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class UnifiedPushMatrixGatewayTestTest {
|
||||
@Test
|
||||
fun `test UnifiedPushMatrixGatewayTest success`() = runTest {
|
||||
val sut = createUnifiedPushMatrixGatewayTest(
|
||||
config = aSessionPushConfig(),
|
||||
discoveryResponse = matrixDiscoveryResponse,
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPushMatrixGatewayTest no config found`() = runTest {
|
||||
val sut = createUnifiedPushMatrixGatewayTest(
|
||||
config = null,
|
||||
discoveryResponse = matrixDiscoveryResponse,
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPushMatrixGatewayTest not valid gateway`() = runTest {
|
||||
val sut = createUnifiedPushMatrixGatewayTest(
|
||||
config = aSessionPushConfig(),
|
||||
discoveryResponse = invalidDiscoveryResponse,
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
|
||||
// Reset the error
|
||||
sut.reset()
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPushMatrixGatewayTest network error`() = runTest {
|
||||
val sut = createUnifiedPushMatrixGatewayTest(
|
||||
config = aSessionPushConfig(),
|
||||
discoveryResponse = { error("Network error") },
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test isRelevant`() = runTest {
|
||||
val sut = createUnifiedPushMatrixGatewayTest()
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = UnifiedPushConfig.NAME))).isTrue()
|
||||
assertThat(sut.isRelevant(TestFilterData(currentPushProviderName = "other"))).isFalse()
|
||||
}
|
||||
|
||||
private fun TestScope.createUnifiedPushMatrixGatewayTest(
|
||||
sessionId: SessionId = A_SESSION_ID,
|
||||
config: Config? = null,
|
||||
discoveryResponse: () -> DiscoveryResponse = matrixDiscoveryResponse,
|
||||
): UnifiedPushMatrixGatewayTest {
|
||||
return UnifiedPushMatrixGatewayTest(
|
||||
sessionId = sessionId,
|
||||
unifiedPushApiFactory = FakeUnifiedPushApiFactory(discoveryResponse),
|
||||
coroutineDispatchers = testCoroutineDispatchers(),
|
||||
unifiedPushSessionPushConfigProvider = FakeUnifiedPushSessionPushConfigProvider(
|
||||
config = { config }
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2024, 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.pushproviders.unifiedpush.troubleshoot
|
||||
|
||||
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.libraries.troubleshoot.test.FakeNotificationTroubleshootNavigator
|
||||
import io.element.android.libraries.troubleshoot.test.runAndTestState
|
||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class UnifiedPushTestTest {
|
||||
@Test
|
||||
fun `test UnifiedPushTest success`() = runTest {
|
||||
val sut = UnifiedPushTest(
|
||||
unifiedPushDistributorProvider = FakeUnifiedPushDistributorProvider(
|
||||
getDistributorsResult = listOf(
|
||||
Distributor("value", "Name"),
|
||||
)
|
||||
),
|
||||
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPushTest error`() = runTest {
|
||||
val providers = FakeUnifiedPushDistributorProvider()
|
||||
val sut = UnifiedPushTest(
|
||||
unifiedPushDistributorProvider = providers,
|
||||
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(
|
||||
executeAction = {
|
||||
providers.setDistributorsResult(
|
||||
listOf(
|
||||
Distributor("value", "Name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
|
||||
// Quick fix
|
||||
backgroundScope.launch {
|
||||
sut.quickFix(this, FakeNotificationTroubleshootNavigator())
|
||||
sut.run(this)
|
||||
}
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Success)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test UnifiedPushTest error and reset`() = runTest {
|
||||
val providers = FakeUnifiedPushDistributorProvider()
|
||||
val sut = UnifiedPushTest(
|
||||
unifiedPushDistributorProvider = providers,
|
||||
openDistributorWebPageAction = FakeOpenDistributorWebPageAction(
|
||||
executeAction = {
|
||||
providers.setDistributorsResult(
|
||||
listOf(
|
||||
Distributor("value", "Name"),
|
||||
)
|
||||
)
|
||||
}
|
||||
),
|
||||
stringProvider = FakeStringProvider(),
|
||||
)
|
||||
sut.runAndTestState {
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.InProgress)
|
||||
val lastItem = awaitItem()
|
||||
assertThat(lastItem.status).isEqualTo(NotificationTroubleshootTestState.Status.Failure(hasQuickFix = true))
|
||||
sut.reset()
|
||||
assertThat(awaitItem().status).isEqualTo(NotificationTroubleshootTestState.Status.Idle(false))
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user