Merge pull request #5139 from element-hq/feature/bma/rageshakeConfigStep2
Let enterprise build be able to override (or disable) the bug report URL.
This commit is contained in:
commit
308cbb4380
67 changed files with 1028 additions and 280 deletions
|
|
@ -9,8 +9,6 @@ package io.element.android.x
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.startup.AppInitializer
|
import androidx.startup.AppInitializer
|
||||||
import io.element.android.appconfig.RageshakeConfig
|
|
||||||
import io.element.android.appconfig.isEnabled
|
|
||||||
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
|
import io.element.android.features.cachecleaner.api.CacheCleanerInitializer
|
||||||
import io.element.android.libraries.di.DaggerComponentOwner
|
import io.element.android.libraries.di.DaggerComponentOwner
|
||||||
import io.element.android.x.di.AppComponent
|
import io.element.android.x.di.AppComponent
|
||||||
|
|
@ -25,9 +23,7 @@ class ElementXApplication : Application(), DaggerComponentOwner {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
AppInitializer.getInstance(this).apply {
|
AppInitializer.getInstance(this).apply {
|
||||||
if (RageshakeConfig.isEnabled) {
|
initializeComponent(CrashInitializer::class.java)
|
||||||
initializeComponent(CrashInitializer::class.java)
|
|
||||||
}
|
|
||||||
initializeComponent(PlatformInitializer::class.java)
|
initializeComponent(PlatformInitializer::class.java)
|
||||||
initializeComponent(CacheCleanerInitializer::class.java)
|
initializeComponent(CacheCleanerInitializer::class.java)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,3 @@ object RageshakeConfig {
|
||||||
*/
|
*/
|
||||||
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
const val MAX_LOG_UPLOAD_SIZE = 50 * 1024 * 1024L
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the rageshake feature is enabled.
|
|
||||||
*/
|
|
||||||
val RageshakeConfig.isEnabled: Boolean
|
|
||||||
get() = BUG_REPORT_URL.isNotEmpty() && BUG_REPORT_APP_NAME.isNotEmpty()
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import io.element.android.appnav.room.RoomFlowNode
|
||||||
import io.element.android.appnav.room.RoomNavigationTarget
|
import io.element.android.appnav.room.RoomNavigationTarget
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
|
|
@ -127,6 +128,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val logoutEntryPoint: LogoutEntryPoint,
|
private val logoutEntryPoint: LogoutEntryPoint,
|
||||||
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
|
||||||
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
|
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
|
||||||
|
private val sessionEnterpriseService: SessionEnterpriseService,
|
||||||
private val networkMonitor: NetworkMonitor,
|
private val networkMonitor: NetworkMonitor,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
|
|
@ -185,7 +187,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
super.onBuilt()
|
super.onBuilt()
|
||||||
|
lifecycleScope.launch {
|
||||||
|
sessionEnterpriseService.init()
|
||||||
|
}
|
||||||
lifecycle.subscribe(
|
lifecycle.subscribe(
|
||||||
onCreate = {
|
onCreate = {
|
||||||
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
appNavigationStateService.onNavigateToSession(id, matrixClient.sessionId)
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit b7ababb9537da8bec254b8ed00b5a4122e9f3e3b
|
Subproject commit 76e10f6fa4db4196df245a3d29131a95d9e60a4d
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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.features.enterprise.api
|
||||||
|
|
||||||
|
sealed interface BugReportUrl {
|
||||||
|
data object UseDefault : BugReportUrl
|
||||||
|
data object Disabled : BugReportUrl
|
||||||
|
data class Custom(
|
||||||
|
val url: String,
|
||||||
|
) : BugReportUrl
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.enterprise.api
|
||||||
|
|
||||||
import io.element.android.compound.tokens.generated.SemanticColors
|
import io.element.android.compound.tokens.generated.SemanticColors
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface EnterpriseService {
|
interface EnterpriseService {
|
||||||
val isEnterpriseBuild: Boolean
|
val isEnterpriseBuild: Boolean
|
||||||
|
|
@ -22,6 +23,8 @@ interface EnterpriseService {
|
||||||
fun firebasePushGateway(): String?
|
fun firebasePushGateway(): String?
|
||||||
fun unifiedPushDefaultPushGateway(): String?
|
fun unifiedPushDefaultPushGateway(): String?
|
||||||
|
|
||||||
|
val bugReportUrlFlow: Flow<BugReportUrl>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ANY_ACCOUNT_PROVIDER = "*"
|
const val ANY_ACCOUNT_PROVIDER = "*"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,6 @@ package io.element.android.features.enterprise.api
|
||||||
|
|
||||||
interface SessionEnterpriseService {
|
interface SessionEnterpriseService {
|
||||||
suspend fun isElementCallAvailable(): Boolean
|
suspend fun isElementCallAvailable(): Boolean
|
||||||
|
|
||||||
|
suspend fun init()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.compound.tokens.generated.SemanticColors
|
import io.element.android.compound.tokens.generated.SemanticColors
|
||||||
import io.element.android.compound.tokens.generated.compoundColorsDark
|
import io.element.android.compound.tokens.generated.compoundColorsDark
|
||||||
import io.element.android.compound.tokens.generated.compoundColorsLight
|
import io.element.android.compound.tokens.generated.compoundColorsLight
|
||||||
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
|
|
@ -31,4 +33,6 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
|
||||||
|
|
||||||
override fun firebasePushGateway(): String? = null
|
override fun firebasePushGateway(): String? = null
|
||||||
override fun unifiedPushDefaultPushGateway(): String? = null
|
override fun unifiedPushDefaultPushGateway(): String? = null
|
||||||
|
|
||||||
|
override val bugReportUrlFlow = flowOf(BugReportUrl.UseDefault)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,6 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService {
|
class DefaultSessionEnterpriseService @Inject constructor() : SessionEnterpriseService {
|
||||||
|
override suspend fun init() = Unit
|
||||||
override suspend fun isElementCallAvailable(): Boolean = true
|
override suspend fun isElementCallAvailable(): Boolean = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,14 @@
|
||||||
package io.element.android.features.enterprise.test
|
package io.element.android.features.enterprise.test
|
||||||
|
|
||||||
import io.element.android.compound.tokens.generated.SemanticColors
|
import io.element.android.compound.tokens.generated.SemanticColors
|
||||||
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.tests.testutils.lambda.lambdaError
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class FakeEnterpriseService(
|
class FakeEnterpriseService(
|
||||||
override val isEnterpriseBuild: Boolean = false,
|
override val isEnterpriseBuild: Boolean = false,
|
||||||
|
|
@ -51,7 +55,6 @@ class FakeEnterpriseService(
|
||||||
return unifiedPushDefaultPushGatewayResult()
|
return unifiedPushDefaultPushGatewayResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
val bugReportUrlMutableFlow = MutableStateFlow<BugReportUrl>(BugReportUrl.UseDefault)
|
||||||
const val A_FAKE_HOMESERVER = "a_fake_homeserver"
|
override val bugReportUrlFlow: Flow<BugReportUrl> = bugReportUrlMutableFlow.asStateFlow()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ import io.element.android.tests.testutils.simulateLongTask
|
||||||
class FakeSessionEnterpriseService(
|
class FakeSessionEnterpriseService(
|
||||||
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
|
||||||
) : SessionEnterpriseService {
|
) : SessionEnterpriseService {
|
||||||
|
override suspend fun init() {
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
|
||||||
isElementCallAvailableResult()
|
isElementCallAvailableResult()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class HomePresenter @Inject constructor(
|
||||||
override fun present(): HomeState {
|
override fun present(): HomeState {
|
||||||
val matrixUser = client.userProfile.collectAsState()
|
val matrixUser = client.userProfile.collectAsState()
|
||||||
val isOnline by syncService.isOnline.collectAsState()
|
val isOnline by syncService.isOnline.collectAsState()
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
val roomListState = roomListPresenter.present()
|
val roomListState = roomListPresenter.present()
|
||||||
val isSpaceFeatureEnabled by remember {
|
val isSpaceFeatureEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Space)
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
import io.element.android.libraries.matrix.test.sync.FakeSyncService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -49,7 +50,7 @@ class HomePresenterTest {
|
||||||
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
|
matrixClient.givenGetProfileResult(matrixClient.sessionId, Result.success(MatrixUser(matrixClient.sessionId, A_USER_NAME, AN_AVATAR_URL)))
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
client = matrixClient,
|
client = matrixClient,
|
||||||
rageshakeFeatureAvailability = { false },
|
rageshakeFeatureAvailability = { flowOf(false) },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -66,6 +67,21 @@ class HomePresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - can report bug`() = runTest {
|
||||||
|
val presenter = createHomePresenter(
|
||||||
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.canReportBug).isFalse()
|
||||||
|
val finalState = awaitItem()
|
||||||
|
assertThat(finalState.canReportBug).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - space feature enabled`() = runTest {
|
fun `present - space feature enabled`() = runTest {
|
||||||
val presenter = createHomePresenter(
|
val presenter = createHomePresenter(
|
||||||
|
|
@ -132,7 +148,7 @@ class HomePresenterTest {
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
client: MatrixClient = FakeMatrixClient(),
|
||||||
syncService: SyncService = FakeSyncService(),
|
syncService: SyncService = FakeSyncService(),
|
||||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(false) },
|
||||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
|
||||||
) = HomePresenter(
|
) = HomePresenter(
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ dependencies {
|
||||||
implementation(projects.libraries.featureflag.api)
|
implementation(projects.libraries.featureflag.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.network)
|
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
@ -44,10 +43,9 @@ dependencies {
|
||||||
implementation(projects.libraries.qrcode)
|
implementation(projects.libraries.qrcode)
|
||||||
implementation(projects.libraries.oidc.api)
|
implementation(projects.libraries.oidc.api)
|
||||||
implementation(projects.libraries.uiUtils)
|
implementation(projects.libraries.uiUtils)
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
implementation(platform(libs.network.retrofit.bom))
|
|
||||||
implementation(libs.androidx.webkit)
|
implementation(libs.androidx.webkit)
|
||||||
implementation(libs.network.retrofit)
|
|
||||||
implementation(libs.serialization.json)
|
implementation(libs.serialization.json)
|
||||||
api(projects.features.login.api)
|
api(projects.features.login.api)
|
||||||
|
|
||||||
|
|
@ -65,6 +63,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.oidc.test)
|
testImplementation(projects.libraries.oidc.test)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
|
testImplementation(projects.libraries.wellknown.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,13 @@ import io.element.android.features.login.api.accesscontrol.AccountProviderAccess
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAccountProviderAccessControl @Inject constructor(
|
class DefaultAccountProviderAccessControl @Inject constructor(
|
||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
private val elementWellknownRetriever: ElementWellknownRetriever,
|
private val wellknownRetriever: WellknownRetriever,
|
||||||
) : AccountProviderAccessControl {
|
) : AccountProviderAccessControl {
|
||||||
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
||||||
assertIsAllowedToConnectToAccountProvider(
|
assertIsAllowedToConnectToAccountProvider(
|
||||||
|
|
@ -37,8 +38,8 @@ class DefaultAccountProviderAccessControl @Inject constructor(
|
||||||
) {
|
) {
|
||||||
if (enterpriseService.isEnterpriseBuild.not()) {
|
if (enterpriseService.isEnterpriseBuild.not()) {
|
||||||
// Ensure that Element Pro is not required for this account provider
|
// Ensure that Element Pro is not required for this account provider
|
||||||
val wellKnown = elementWellknownRetriever.retrieve(
|
val wellKnown = wellknownRetriever.getElementWellKnown(
|
||||||
accountProviderUrl = accountProviderUrl.ensureProtocol(),
|
baseUrl = accountProviderUrl.ensureProtocol(),
|
||||||
)
|
)
|
||||||
if (wellKnown?.enforceElementPro == true) {
|
if (wellKnown?.enforceElementPro == true) {
|
||||||
throw AccountProviderAccessException.NeedElementProException(
|
throw AccountProviderAccessException.NeedElementProException(
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.login.impl.accesscontrol
|
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.features.login.impl.resolver.network.ElementWellKnown
|
|
||||||
import io.element.android.features.login.impl.resolver.network.WellknownAPI
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.network.RetrofitFactory
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
interface ElementWellknownRetriever {
|
|
||||||
suspend fun retrieve(accountProviderUrl: String): ElementWellKnown?
|
|
||||||
}
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultElementWellknownRetriever @Inject constructor(
|
|
||||||
private val retrofitFactory: RetrofitFactory,
|
|
||||||
) : ElementWellknownRetriever {
|
|
||||||
override suspend fun retrieve(accountProviderUrl: String): ElementWellKnown? {
|
|
||||||
val wellknownApi = try {
|
|
||||||
retrofitFactory.create(accountProviderUrl)
|
|
||||||
.create(WellknownAPI::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// If the base URL is not valid, we cannot retrieve the well-known data
|
|
||||||
Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return try {
|
|
||||||
wellknownApi.getElementWellKnown()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to retrieve Element well-known data for $accountProviderUrl")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,13 +7,14 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver
|
package io.element.android.features.login.impl.resolver
|
||||||
|
|
||||||
import io.element.android.features.login.impl.resolver.network.WellknownRequest
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.coroutine.parallelMap
|
import io.element.android.libraries.core.coroutine.parallelMap
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.core.uri.isValidUrl
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
@ -27,7 +28,7 @@ import javax.inject.Inject
|
||||||
*/
|
*/
|
||||||
class HomeserverResolver @Inject constructor(
|
class HomeserverResolver @Inject constructor(
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val wellknownRequest: WellknownRequest,
|
private val wellknownRetriever: WellknownRetriever,
|
||||||
) {
|
) {
|
||||||
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
fun resolve(userInput: String): Flow<List<HomeserverData>> = flow {
|
||||||
val flowContext = currentCoroutineContext()
|
val flowContext = currentCoroutineContext()
|
||||||
|
|
@ -41,7 +42,7 @@ class HomeserverResolver @Inject constructor(
|
||||||
list.parallelMap { url ->
|
list.parallelMap { url ->
|
||||||
val wellKnown = tryOrNull {
|
val wellKnown = tryOrNull {
|
||||||
withTimeout(5000) {
|
withTimeout(5000) {
|
||||||
wellknownRequest.execute(url)
|
wellknownRetriever.getWellKnown(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isValid = wellKnown?.isValid().orFalse()
|
val isValid = wellKnown?.isValid().orFalse()
|
||||||
|
|
@ -86,3 +87,7 @@ class HomeserverResolver @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun WellKnown.isValid(): Boolean {
|
||||||
|
return homeServer?.baseURL?.isNotBlank().orFalse()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
|
||||||
import io.element.android.libraries.di.AppScope
|
|
||||||
import io.element.android.libraries.network.RetrofitFactory
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultWellknownRequest @Inject constructor(
|
|
||||||
private val retrofitFactory: RetrofitFactory,
|
|
||||||
) : WellknownRequest {
|
|
||||||
/**
|
|
||||||
* Return the WellKnown data, if found.
|
|
||||||
* @param baseUrl for instance https://matrix.org
|
|
||||||
*/
|
|
||||||
override suspend fun execute(baseUrl: String): WellKnown {
|
|
||||||
val wellknownApi = retrofitFactory.create(baseUrl)
|
|
||||||
.create(WellknownAPI::class.java)
|
|
||||||
return wellknownApi.getWellKnown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
|
||||||
|
|
||||||
interface WellknownRequest {
|
|
||||||
/**
|
|
||||||
* Return the WellKnown data, or throw an error if not found.
|
|
||||||
* @param baseUrl for instance https://matrix.org
|
|
||||||
*/
|
|
||||||
suspend fun execute(baseUrl: String): WellKnown
|
|
||||||
}
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.login.impl.screens.onboarding
|
package io.element.android.features.login.impl.screens.onboarding
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
|
|
@ -82,7 +83,7 @@ class OnBoardingPresenter @AssistedInject constructor(
|
||||||
value = linkAccountProvider == null &&
|
value = linkAccountProvider == null &&
|
||||||
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
|
||||||
}
|
}
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
var showReportBug by rememberSaveable { mutableStateOf(false) }
|
var showReportBug by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val loginMode by loginHelper.collectLoginMode()
|
val loginMode by loginHelper.collectLoginMode()
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,10 @@ package io.element.android.features.login.impl.web
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.features.login.impl.resolver.network.WellknownAPI
|
|
||||||
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.network.RetrofitFactory
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface WebClientUrlForAuthenticationRetriever {
|
interface WebClientUrlForAuthenticationRetriever {
|
||||||
|
|
@ -24,24 +22,16 @@ interface WebClientUrlForAuthenticationRetriever {
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor(
|
class DefaultWebClientUrlForAuthenticationRetriever @Inject constructor(
|
||||||
private val retrofitFactory: RetrofitFactory,
|
private val wellknownRetriever: WellknownRetriever,
|
||||||
) : WebClientUrlForAuthenticationRetriever {
|
) : WebClientUrlForAuthenticationRetriever {
|
||||||
override suspend fun retrieve(homeServerUrl: String): String {
|
override suspend fun retrieve(homeServerUrl: String): String {
|
||||||
if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) {
|
if (homeServerUrl != AuthenticationConfig.MATRIX_ORG_URL) {
|
||||||
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
||||||
throw AccountCreationNotSupported()
|
throw AccountCreationNotSupported()
|
||||||
}
|
}
|
||||||
val wellknownApi = retrofitFactory.create(homeServerUrl)
|
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl)
|
||||||
.create(WellknownAPI::class.java)
|
?: throw AccountCreationNotSupported()
|
||||||
val result = try {
|
val registrationHelperUrl = wellknown.registrationHelperUrl
|
||||||
wellknownApi.getElementWellKnown()
|
|
||||||
} catch (e: retrofit2.HttpException) {
|
|
||||||
throw when {
|
|
||||||
e.code() == HttpURLConnection.HTTP_NOT_FOUND -> AccountCreationNotSupported()
|
|
||||||
else -> e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val registrationHelperUrl = result.registrationHelperUrl
|
|
||||||
return if (registrationHelperUrl != null) {
|
return if (registrationHelperUrl != null) {
|
||||||
registrationHelperUrl.toUri()
|
registrationHelperUrl.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ package io.element.android.features.login.impl.accesscontrol
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.features.login.impl.resolver.network.ElementWellKnown
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -24,7 +26,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -37,7 +39,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
// false here.
|
// false here.
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -49,7 +51,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = false,
|
enforceElementPro = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -61,7 +63,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = null,
|
enforceElementPro = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -84,7 +86,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = false,
|
enforceElementPro = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -96,7 +98,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -108,7 +110,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = false,
|
enforceElementPro = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -121,7 +123,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -134,7 +136,7 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = ElementWellKnown(
|
elementWellKnown = anElementWellKnown(
|
||||||
enforceElementPro = false,
|
enforceElementPro = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -152,8 +154,8 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
||||||
defaultHomeserverListResult = { allowedAccountProviders },
|
defaultHomeserverListResult = { allowedAccountProviders },
|
||||||
),
|
),
|
||||||
elementWellknownRetriever = FakeElementWellknownRetriever(
|
wellknownRetriever = FakeWellknownRetriever(
|
||||||
retrieveResult = { elementWellKnown }
|
getElementWellKnownResult = { elementWellKnown },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.features.login.impl.accesscontrol
|
|
||||||
|
|
||||||
import io.element.android.features.login.impl.resolver.network.ElementWellKnown
|
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
|
||||||
|
|
||||||
class FakeElementWellknownRetriever(
|
|
||||||
private val retrieveResult: (String) -> ElementWellKnown? = { null },
|
|
||||||
) : ElementWellknownRetriever {
|
|
||||||
override suspend fun retrieve(accountProviderUrl: String): ElementWellKnown? = simulateLongTask {
|
|
||||||
retrieveResult(accountProviderUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -11,17 +11,18 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
import io.element.android.features.login.impl.accountprovider.AccountProvider
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.features.login.impl.resolver.network.ElementWellKnown
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
|
import io.element.android.features.wellknown.test.anElementWellKnown
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
import io.element.android.libraries.matrix.test.A_HOMESERVER
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -113,14 +114,14 @@ class ChangeServerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change server element pro required error`() = runTest {
|
fun `present - change server element pro required error`() = runTest {
|
||||||
val retrieveResult = lambdaRecorder<String, ElementWellKnown> {
|
val getElementWellKnownResult = lambdaRecorder<String, ElementWellKnown> {
|
||||||
ElementWellKnown(
|
anElementWellKnown(
|
||||||
enforceElementPro = true,
|
enforceElementPro = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
createPresenter(
|
createPresenter(
|
||||||
elementWellknownRetriever = FakeElementWellknownRetriever(
|
wellknownRetriever = FakeWellknownRetriever(
|
||||||
retrieveResult = retrieveResult,
|
getElementWellKnownResult = getElementWellKnownResult,
|
||||||
),
|
),
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -136,7 +137,7 @@ class ChangeServerPresenterTest {
|
||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
||||||
).isEqualTo("io.element.enterprise")
|
).isEqualTo("io.element.enterprise")
|
||||||
retrieveResult.assertions()
|
getElementWellKnownResult.assertions()
|
||||||
.isCalledOnce()
|
.isCalledOnce()
|
||||||
.with(value(A_HOMESERVER_URL.ensureProtocol()))
|
.with(value(A_HOMESERVER_URL.ensureProtocol()))
|
||||||
}
|
}
|
||||||
|
|
@ -146,13 +147,13 @@ class ChangeServerPresenterTest {
|
||||||
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
) = ChangeServerPresenter(
|
) = ChangeServerPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
elementWellknownRetriever = elementWellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
|
||||||
|
|
||||||
class FakeWellknownRequest : WellknownRequest {
|
|
||||||
private var resultMap: Map<String, WellKnown> = emptyMap()
|
|
||||||
fun givenResultMap(map: Map<String, WellKnown>) {
|
|
||||||
resultMap = map
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun execute(baseUrl: String): WellKnown {
|
|
||||||
return resultMap[baseUrl] ?: error("No result provided for $baseUrl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,11 +13,10 @@ import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.DefaultLoginUserStory
|
import io.element.android.features.login.impl.DefaultLoginUserStory
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.login.LoginHelper
|
import io.element.android.features.login.impl.login.LoginHelper
|
||||||
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever
|
||||||
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever
|
||||||
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
|
@ -34,8 +33,11 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.oidc.api.OidcActionFlow
|
import io.element.android.libraries.oidc.api.OidcActionFlow
|
||||||
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
import io.element.android.libraries.oidc.test.customtab.FakeOidcActionFlow
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -78,7 +80,6 @@ class OnBoardingPresenterTest {
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||||
),
|
),
|
||||||
rageshakeFeatureAvailability = { true },
|
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|
@ -94,7 +95,7 @@ class OnBoardingPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest {
|
fun `present - clicking on version 7 times has no effect if rageshake not available`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
rageshakeFeatureAvailability = { false },
|
rageshakeFeatureAvailability = { flowOf(false) },
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -238,8 +239,8 @@ private fun createPresenter(
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
rageshakeFeatureAvailability: () -> Boolean = { true },
|
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
||||||
loginHelper: LoginHelper = createLoginHelper(),
|
loginHelper: LoginHelper = createLoginHelper(),
|
||||||
) = OnBoardingPresenter(
|
) = OnBoardingPresenter(
|
||||||
params = params,
|
params = params,
|
||||||
|
|
@ -248,7 +249,7 @@ private fun createPresenter(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
elementWellknownRetriever = elementWellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginHelper = loginHelper,
|
loginHelper = loginHelper,
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,15 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accesscontrol.ElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.accesscontrol.FakeElementWellknownRetriever
|
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
import io.element.android.features.login.impl.qrcode.FakeQrCodeLoginManager
|
||||||
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrCodeLoginStep
|
||||||
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
import io.element.android.libraries.matrix.api.auth.qrlogin.QrLoginException
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginData
|
||||||
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
import io.element.android.libraries.matrix.test.auth.qrlogin.FakeMatrixQrCodeLoginDataFactory
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
|
@ -162,14 +162,14 @@ class QrCodeScanPresenterTest {
|
||||||
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
|
||||||
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
qrCodeLoginManager: FakeQrCodeLoginManager = FakeQrCodeLoginManager(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
elementWellknownRetriever: ElementWellknownRetriever = FakeElementWellknownRetriever(),
|
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
||||||
) = QrCodeScanPresenter(
|
) = QrCodeScanPresenter(
|
||||||
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
qrCodeLoginDataFactory = qrCodeLoginDataFactory,
|
||||||
qrCodeLoginManager = qrCodeLoginManager,
|
qrCodeLoginManager = qrCodeLoginManager,
|
||||||
coroutineDispatchers = coroutineDispatchers,
|
coroutineDispatchers = coroutineDispatchers,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
elementWellknownRetriever = elementWellknownRetriever,
|
wellknownRetriever = wellknownRetriever,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
import io.element.android.features.login.impl.changeserver.aChangeServerState
|
||||||
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
import io.element.android.features.login.impl.resolver.HomeserverResolver
|
||||||
import io.element.android.features.login.impl.resolver.network.FakeWellknownRequest
|
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
||||||
import io.element.android.features.login.impl.resolver.network.WellKnown
|
|
||||||
import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -30,9 +32,9 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - initial state`() = runTest {
|
fun `present - initial state`() = runTest {
|
||||||
val fakeWellknownRequest = FakeWellknownRequest()
|
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -46,9 +48,9 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text no result`() = runTest {
|
fun `present - enter text no result`() = runTest {
|
||||||
val fakeWellknownRequest = FakeWellknownRequest()
|
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -66,9 +68,9 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter valid url no wellknown`() = runTest {
|
fun `present - enter valid url no wellknown`() = runTest {
|
||||||
val fakeWellknownRequest = FakeWellknownRequest()
|
val fakeWellknownRetriever = FakeWellknownRetriever()
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -92,14 +94,20 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text one result with wellknown`() = runTest {
|
fun `present - enter text one result with wellknown`() = runTest {
|
||||||
val fakeWellknownRequest = FakeWellknownRequest()
|
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
|
||||||
fakeWellknownRequest.givenResultMap(
|
when (it) {
|
||||||
mapOf(
|
"https://test.org" -> error("not found")
|
||||||
"https://test.io" to aWellKnown(),
|
"https://test.com" -> error("not found")
|
||||||
)
|
"https://test.io" -> aWellKnown()
|
||||||
|
"https://test" -> error("not found")
|
||||||
|
else -> error("should not happen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val fakeWellknownRetriever = FakeWellknownRetriever(
|
||||||
|
getWellKnownResult = getWellKnownResult,
|
||||||
)
|
)
|
||||||
val presenter = SearchAccountProviderPresenter(
|
val presenter = SearchAccountProviderPresenter(
|
||||||
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest),
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
changeServerPresenter = { aChangeServerState() }
|
changeServerPresenter = { aChangeServerState() }
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -118,6 +126,65 @@ class SearchAccountProviderPresenterTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
getWellKnownResult.assertions().isCalledExactly(4)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://test.org")),
|
||||||
|
listOf(value("https://test.com")),
|
||||||
|
listOf(value("https://test.io")),
|
||||||
|
listOf(value("https://test")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - enter text two results with wellknown`() = runTest {
|
||||||
|
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
|
||||||
|
when (it) {
|
||||||
|
"https://test.org" -> aWellKnown()
|
||||||
|
"https://test.com" -> error("not found")
|
||||||
|
"https://test.io" -> aWellKnown()
|
||||||
|
"https://test" -> error("not found")
|
||||||
|
else -> error("should not happen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val fakeWellknownRetriever = FakeWellknownRetriever(
|
||||||
|
getWellKnownResult = getWellKnownResult,
|
||||||
|
)
|
||||||
|
val presenter = SearchAccountProviderPresenter(
|
||||||
|
homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever),
|
||||||
|
changeServerPresenter = { aChangeServerState() }
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test"))
|
||||||
|
val withInputState = awaitItem()
|
||||||
|
assertThat(withInputState.userInput).isEqualTo("test")
|
||||||
|
assertThat(initialState.userInputResult).isEqualTo(AsyncData.Uninitialized)
|
||||||
|
assertThat(awaitItem().userInputResult).isInstanceOf(AsyncData.Loading::class.java)
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||||
|
AsyncData.Success(
|
||||||
|
listOf(
|
||||||
|
aHomeserverData(homeserverUrl = "https://test.org"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertThat(awaitItem().userInputResult).isEqualTo(
|
||||||
|
AsyncData.Success(
|
||||||
|
listOf(
|
||||||
|
aHomeserverData(homeserverUrl = "https://test.org"),
|
||||||
|
aHomeserverData(homeserverUrl = "https://test.io"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
getWellKnownResult.assertions().isCalledExactly(4)
|
||||||
|
.withSequence(
|
||||||
|
listOf(value("https://test.org")),
|
||||||
|
listOf(value("https://test.com")),
|
||||||
|
listOf(value("https://test.io")),
|
||||||
|
listOf(value("https://test")),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ class PreferencesRootPresenter @Inject constructor(
|
||||||
var canDeactivateAccount by remember {
|
var canDeactivateAccount by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
val canReportBug = remember { rageshakeFeatureAvailability.isAvailable() }
|
val canReportBug by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
canDeactivateAccount = matrixClient.canDeactivateAccount()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -102,7 +103,7 @@ class PreferencesRootPresenterTest {
|
||||||
)
|
)
|
||||||
createPresenter(
|
createPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
rageshakeFeatureAvailability = { false },
|
rageshakeFeatureAvailability = { flowOf(false) },
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.canReportBug).isFalse()
|
assertThat(initialState.canReportBug).isFalse()
|
||||||
|
|
@ -119,7 +120,7 @@ class PreferencesRootPresenterTest {
|
||||||
val indicatorService = FakeIndicatorService()
|
val indicatorService = FakeIndicatorService()
|
||||||
createPresenter(
|
createPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
rageshakeFeatureAvailability = { false },
|
rageshakeFeatureAvailability = { flowOf(false) },
|
||||||
indicatorService = indicatorService,
|
indicatorService = indicatorService,
|
||||||
).test {
|
).test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -185,7 +186,7 @@ class PreferencesRootPresenterTest {
|
||||||
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
matrixClient: FakeMatrixClient = FakeMatrixClient(),
|
||||||
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
|
showDeveloperSettingsProvider: ShowDeveloperSettingsProvider = ShowDeveloperSettingsProvider(aBuildMeta(BuildType.DEBUG)),
|
||||||
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { true },
|
rageshakeFeatureAvailability: RageshakeFeatureAvailability = RageshakeFeatureAvailability { flowOf(true) },
|
||||||
indicatorService: IndicatorService = FakeIndicatorService(),
|
indicatorService: IndicatorService = FakeIndicatorService(),
|
||||||
) = PreferencesRootPresenter(
|
) = PreferencesRootPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.api
|
package io.element.android.features.rageshake.api
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
fun interface RageshakeFeatureAvailability {
|
fun interface RageshakeFeatureAvailability {
|
||||||
fun isAvailable(): Boolean
|
fun isAvailable(): Flow<Boolean>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ setupAnvil()
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
|
@ -50,6 +51,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.test.mockk)
|
testImplementation(libs.test.mockk)
|
||||||
|
testImplementation(projects.features.enterprise.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.sessionStorage.implMemory)
|
testImplementation(projects.libraries.sessionStorage.implMemory)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,19 @@
|
||||||
package io.element.android.features.rageshake.impl
|
package io.element.android.features.rageshake.impl
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.RageshakeConfig
|
|
||||||
import io.element.android.appconfig.isEnabled
|
|
||||||
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
|
||||||
|
import io.element.android.features.rageshake.impl.reporter.BugReporterUrlProvider
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultRageshakeFeatureAvailability @Inject constructor() : RageshakeFeatureAvailability {
|
class DefaultRageshakeFeatureAvailability @Inject constructor(
|
||||||
override fun isAvailable(): Boolean {
|
private val bugReporterUrlProvider: BugReporterUrlProvider,
|
||||||
return RageshakeConfig.isEnabled
|
) : RageshakeFeatureAvailability {
|
||||||
|
override fun isAvailable(): Flow<Boolean> {
|
||||||
|
return bugReporterUrlProvider.provide()
|
||||||
|
.map { it != null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,13 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.crash
|
package io.element.android.features.rageshake.impl.crash
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
|
@ -19,6 +22,8 @@ import io.element.android.features.rageshake.api.crash.CrashDetectionState
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -32,12 +37,15 @@ class DefaultCrashDetectionPresenter @Inject constructor(
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): CrashDetectionState {
|
override fun present(): CrashDetectionState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val crashDetected = remember {
|
val crashDetected by remember {
|
||||||
if (rageshakeFeatureAvailability.isAvailable()) {
|
rageshakeFeatureAvailability.isAvailable()
|
||||||
crashDataStore.appHasCrashed()
|
.flatMapLatest { isAvailable ->
|
||||||
} else {
|
if (isAvailable) {
|
||||||
flowOf(false)
|
crashDataStore.appHasCrashed()
|
||||||
}
|
} else {
|
||||||
|
flowOf(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}.collectAsState(false)
|
}.collectAsState(false)
|
||||||
|
|
||||||
fun handleEvents(event: CrashDetectionEvents) {
|
fun handleEvents(event: CrashDetectionEvents) {
|
||||||
|
|
@ -49,7 +57,7 @@ class DefaultCrashDetectionPresenter @Inject constructor(
|
||||||
|
|
||||||
return CrashDetectionState(
|
return CrashDetectionState(
|
||||||
appName = buildMeta.applicationName,
|
appName = buildMeta.applicationName,
|
||||||
crashDetected = crashDetected.value,
|
crashDetected = crashDetected,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ class DefaultRageshakePreferencesPresenter @Inject constructor(
|
||||||
val isSupported: MutableState<Boolean> = rememberSaveable {
|
val isSupported: MutableState<Boolean> = rememberSaveable {
|
||||||
mutableStateOf(rageshake.isAvailable())
|
mutableStateOf(rageshake.isAvailable())
|
||||||
}
|
}
|
||||||
val isFeatureAvailable = remember { rageshakeFeatureAvailability.isAvailable() }
|
val isFeatureAvailable by remember { rageshakeFeatureAvailability.isAvailable() }.collectAsState(false)
|
||||||
val isEnabled by remember {
|
val isEnabled by remember {
|
||||||
rageshakeDataStore.isEnabled()
|
rageshakeDataStore.isEnabled()
|
||||||
}.collectAsState(initial = false)
|
}.collectAsState(initial = false)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
/*
|
||||||
|
* 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.features.rageshake.impl.reporter
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.appconfig.RageshakeConfig
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
fun interface BugReportAppNameProvider {
|
||||||
|
fun provide(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultBugReportAppNameProvider @Inject constructor() : BugReportAppNameProvider {
|
||||||
|
override fun provide(): String = RageshakeConfig.BUG_REPORT_APP_NAME
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.reporter
|
package io.element.android.features.rageshake.impl.reporter
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
fun interface BugReporterUrlProvider {
|
fun interface BugReporterUrlProvider {
|
||||||
fun provide(): HttpUrl
|
fun provide(): Flow<HttpUrl?>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,12 @@ class DefaultBugReporter @Inject constructor(
|
||||||
canContact: Boolean,
|
canContact: Boolean,
|
||||||
listener: BugReporterListener,
|
listener: BugReporterListener,
|
||||||
) {
|
) {
|
||||||
|
val url = bugReporterUrlProvider.provide().first()
|
||||||
|
if (url == null) {
|
||||||
|
// It should not happen, but if the URL is null, we cannot proceed
|
||||||
|
Timber.e("## sendBugReport() : bug report URL is null")
|
||||||
|
error("Bug report URL is null, cannot send bug report")
|
||||||
|
}
|
||||||
// enumerate files to delete
|
// enumerate files to delete
|
||||||
val bugReportFiles: MutableList<File> = ArrayList()
|
val bugReportFiles: MutableList<File> = ArrayList()
|
||||||
var response: Response? = null
|
var response: Response? = null
|
||||||
|
|
@ -243,7 +249,7 @@ class DefaultBugReporter @Inject constructor(
|
||||||
}
|
}
|
||||||
// build the request
|
// build the request
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(bugReporterUrlProvider.provide())
|
.url(url)
|
||||||
.post(requestBody)
|
.post(requestBody)
|
||||||
.build()
|
.build()
|
||||||
var errorMessage: String? = null
|
var errorMessage: String? = null
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,31 @@ package io.element.android.features.rageshake.impl.reporter
|
||||||
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.appconfig.RageshakeConfig
|
import io.element.android.appconfig.RageshakeConfig
|
||||||
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultBugReporterUrlProvider @Inject constructor() : BugReporterUrlProvider {
|
class DefaultBugReporterUrlProvider @Inject constructor(
|
||||||
override fun provide(): HttpUrl {
|
private val bugReportAppNameProvider: BugReportAppNameProvider,
|
||||||
return RageshakeConfig.BUG_REPORT_URL.toHttpUrl()
|
private val enterpriseService: EnterpriseService,
|
||||||
|
) : BugReporterUrlProvider {
|
||||||
|
override fun provide(): Flow<HttpUrl?> {
|
||||||
|
if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null)
|
||||||
|
return enterpriseService.bugReportUrlFlow
|
||||||
|
.map { bugReportUrl ->
|
||||||
|
when (bugReportUrl) {
|
||||||
|
is BugReportUrl.Custom -> bugReportUrl.url
|
||||||
|
BugReportUrl.Disabled -> null
|
||||||
|
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { it?.toHttpUrl() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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.features.rageshake.impl
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultRageshakeFeatureAvailabilityTest {
|
||||||
|
@Test
|
||||||
|
fun `test isAvailable returns true when bug reporter URL is provided`() = runTest {
|
||||||
|
val flow = MutableStateFlow<HttpUrl?>(null)
|
||||||
|
val sut = DefaultRageshakeFeatureAvailability(
|
||||||
|
bugReporterUrlProvider = { flow },
|
||||||
|
)
|
||||||
|
sut.isAvailable().test {
|
||||||
|
assertThat(awaitItem()).isFalse()
|
||||||
|
flow.value = "https://example.com/bugreport".toHttpUrl()
|
||||||
|
assertThat(awaitItem()).isTrue()
|
||||||
|
flow.value = null
|
||||||
|
assertThat(awaitItem()).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ class FakeCrashDataStore(
|
||||||
|
|
||||||
override fun setCrashData(crashData: String) {
|
override fun setCrashData(crashData: String) {
|
||||||
crashDataFlow.value = crashData
|
crashDataFlow.value = crashData
|
||||||
|
appHasCrashedFlow.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetAppHasCrashed() {
|
override suspend fun resetAppHasCrashed() {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -55,7 +58,7 @@ class CrashDetectionPresenterTest {
|
||||||
fun `present - initial state crash is ignored if the feature is not available`() = runTest {
|
fun `present - initial state crash is ignored if the feature is not available`() = runTest {
|
||||||
val presenter = createPresenter(
|
val presenter = createPresenter(
|
||||||
FakeCrashDataStore(appHasCrashed = true),
|
FakeCrashDataStore(appHasCrashed = true),
|
||||||
isFeatureAvailable = false,
|
isFeatureAvailableFlow = flowOf(false),
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -97,13 +100,42 @@ class CrashDetectionPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - crashDetected is false if the feature is not available`() = runTest {
|
||||||
|
val isFeatureAvailableFlow = MutableStateFlow(false)
|
||||||
|
val crashDataStore = FakeCrashDataStore(appHasCrashed = false)
|
||||||
|
val presenter = createPresenter(
|
||||||
|
crashDataStore = crashDataStore,
|
||||||
|
isFeatureAvailableFlow = isFeatureAvailableFlow,
|
||||||
|
)
|
||||||
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
presenter.present()
|
||||||
|
}.test {
|
||||||
|
val initialState = awaitItem()
|
||||||
|
assertThat(initialState.crashDetected).isFalse()
|
||||||
|
crashDataStore.setCrashData("Some crash data")
|
||||||
|
// No new state
|
||||||
|
crashDataStore.resetAppHasCrashed()
|
||||||
|
// No new state
|
||||||
|
isFeatureAvailableFlow.value = true
|
||||||
|
crashDataStore.setCrashData("Some crash data")
|
||||||
|
assertThat(awaitItem().crashDetected).isTrue()
|
||||||
|
crashDataStore.resetAppHasCrashed()
|
||||||
|
assertThat(awaitItem().crashDetected).isFalse()
|
||||||
|
crashDataStore.setCrashData("Some crash data")
|
||||||
|
assertThat(awaitItem().crashDetected).isTrue()
|
||||||
|
isFeatureAvailableFlow.value = false
|
||||||
|
assertThat(awaitItem().crashDetected).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createPresenter(
|
private fun createPresenter(
|
||||||
crashDataStore: FakeCrashDataStore = FakeCrashDataStore(),
|
crashDataStore: FakeCrashDataStore = FakeCrashDataStore(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
isFeatureAvailable: Boolean = true,
|
isFeatureAvailableFlow: Flow<Boolean> = flowOf(true),
|
||||||
) = DefaultCrashDetectionPresenter(
|
) = DefaultCrashDetectionPresenter(
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
crashDataStore = crashDataStore,
|
crashDataStore = crashDataStore,
|
||||||
rageshakeFeatureAvailability = { isFeatureAvailable },
|
rageshakeFeatureAvailability = { isFeatureAvailableFlow },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.BeforeClass
|
import org.junit.BeforeClass
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -52,7 +53,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||||
rageshake = rageshake,
|
rageshake = rageshake,
|
||||||
rageshakeDataStore = rageshakeDataStore,
|
rageshakeDataStore = rageshakeDataStore,
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -77,7 +78,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||||
rageshake = rageshake,
|
rageshake = rageshake,
|
||||||
rageshakeDataStore = rageshakeDataStore,
|
rageshakeDataStore = rageshakeDataStore,
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -103,7 +104,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||||
rageshake = rageshake,
|
rageshake = rageshake,
|
||||||
rageshakeDataStore = rageshakeDataStore,
|
rageshakeDataStore = rageshakeDataStore,
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -138,7 +139,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||||
rageshake = rageshake,
|
rageshake = rageshake,
|
||||||
rageshakeDataStore = rageshakeDataStore,
|
rageshakeDataStore = rageshakeDataStore,
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
@ -173,7 +174,7 @@ class RageshakeDetectionPresenterTest {
|
||||||
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
preferencesPresenter = DefaultRageshakePreferencesPresenter(
|
||||||
rageshake = rageshake,
|
rageshake = rageshake,
|
||||||
rageshakeDataStore = rageshakeDataStore,
|
rageshakeDataStore = rageshakeDataStore,
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import io.element.android.features.rageshake.impl.rageshake.A_SENSITIVITY
|
||||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
|
import io.element.android.features.rageshake.impl.rageshake.FakeRageShake
|
||||||
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
|
import io.element.android.features.rageshake.impl.rageshake.FakeRageshakeDataStore
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -29,7 +30,7 @@ class RageshakePreferencesPresenterTest {
|
||||||
val presenter = DefaultRageshakePreferencesPresenter(
|
val presenter = DefaultRageshakePreferencesPresenter(
|
||||||
FakeRageShake(isAvailableValue = true),
|
FakeRageShake(isAvailableValue = true),
|
||||||
FakeRageshakeDataStore(isEnabled = true),
|
FakeRageshakeDataStore(isEnabled = true),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -46,7 +47,7 @@ class RageshakePreferencesPresenterTest {
|
||||||
val presenter = DefaultRageshakePreferencesPresenter(
|
val presenter = DefaultRageshakePreferencesPresenter(
|
||||||
FakeRageShake(isAvailableValue = false),
|
FakeRageShake(isAvailableValue = false),
|
||||||
FakeRageshakeDataStore(isEnabled = true),
|
FakeRageshakeDataStore(isEnabled = true),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -63,7 +64,7 @@ class RageshakePreferencesPresenterTest {
|
||||||
val presenter = DefaultRageshakePreferencesPresenter(
|
val presenter = DefaultRageshakePreferencesPresenter(
|
||||||
FakeRageShake(isAvailableValue = true),
|
FakeRageShake(isAvailableValue = true),
|
||||||
FakeRageshakeDataStore(isEnabled = true),
|
FakeRageshakeDataStore(isEnabled = true),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
@ -83,7 +84,7 @@ class RageshakePreferencesPresenterTest {
|
||||||
val presenter = DefaultRageshakePreferencesPresenter(
|
val presenter = DefaultRageshakePreferencesPresenter(
|
||||||
FakeRageShake(isAvailableValue = true),
|
FakeRageShake(isAvailableValue = true),
|
||||||
FakeRageshakeDataStore(isEnabled = true),
|
FakeRageshakeDataStore(isEnabled = true),
|
||||||
rageshakeFeatureAvailability = { true },
|
rageshakeFeatureAvailability = { flowOf(true) },
|
||||||
)
|
)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionSt
|
||||||
import io.element.android.libraries.sessionstorage.test.aSessionData
|
import io.element.android.libraries.sessionstorage.test.aSessionData
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.MultipartReader
|
import okhttp3.MultipartReader
|
||||||
|
|
@ -464,7 +465,7 @@ class DefaultBugReporterTest {
|
||||||
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
|
userAgentProvider = DefaultUserAgentProvider(buildMeta, FakeSdkMetadata("123456789")),
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
bugReporterUrlProvider = { server.url("/") },
|
bugReporterUrlProvider = { flowOf(server.url("/")) },
|
||||||
sdkMetadata = FakeSdkMetadata("123456789"),
|
sdkMetadata = FakeSdkMetadata("123456789"),
|
||||||
matrixClientProvider = matrixClientProvider,
|
matrixClientProvider = matrixClientProvider,
|
||||||
tracingService = tracingService,
|
tracingService = tracingService,
|
||||||
|
|
|
||||||
|
|
@ -7,18 +7,44 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.reporter
|
package io.element.android.features.rageshake.impl.reporter
|
||||||
|
|
||||||
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appconfig.RageshakeConfig
|
import io.element.android.appconfig.RageshakeConfig
|
||||||
|
import io.element.android.features.enterprise.api.BugReportUrl
|
||||||
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class DefaultBugReporterUrlProviderTest {
|
class DefaultBugReporterUrlProviderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test DefaultBugReporterUrlProvider`() {
|
fun `provide return values when there is an rageshake app name`() = runTest {
|
||||||
val sut = DefaultBugReporterUrlProvider()
|
val enterpriseService = FakeEnterpriseService()
|
||||||
if (RageshakeConfig.BUG_REPORT_URL.isNotEmpty()) {
|
val sut = DefaultBugReporterUrlProvider(
|
||||||
val result = sut.provide()
|
bugReportAppNameProvider = { "rageshakeAppName" },
|
||||||
assertThat(result).isEqualTo(RageshakeConfig.BUG_REPORT_URL.toHttpUrl())
|
enterpriseService = enterpriseService,
|
||||||
|
)
|
||||||
|
sut.provide().test {
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }?.toHttpUrl()
|
||||||
|
)
|
||||||
|
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Disabled)
|
||||||
|
assertThat(awaitItem()).isNull()
|
||||||
|
enterpriseService.bugReportUrlMutableFlow.emit(BugReportUrl.Custom("https://aURL.org"))
|
||||||
|
assertThat(awaitItem()).isEqualTo("https://aURL.org".toHttpUrl())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `provide return null when there is no rageshake app name`() = runTest {
|
||||||
|
val enterpriseService = FakeEnterpriseService()
|
||||||
|
val sut = DefaultBugReporterUrlProvider(
|
||||||
|
bugReportAppNameProvider = { "" },
|
||||||
|
enterpriseService = enterpriseService,
|
||||||
|
)
|
||||||
|
sut.provide().test {
|
||||||
|
assertThat(awaitItem()).isNull()
|
||||||
|
awaitComplete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,6 @@ package io.element.android.libraries.core.coroutine
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
* Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the first element of the flow that is an instance of [T], waiting for it if necessary.
|
* Returns the first element of the flow that is an instance of [T], waiting for it if necessary.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
13
libraries/wellknown/api/build.gradle.kts
Normal file
13
libraries/wellknown/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.wellknown.api"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.wellknown.api
|
||||||
|
|
||||||
|
data class ElementWellKnown(
|
||||||
|
val registrationHelperUrl: String?,
|
||||||
|
val enforceElementPro: Boolean?,
|
||||||
|
val rageshakeUrl: String?,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.api
|
||||||
|
|
||||||
|
interface SessionWellknownRetriever {
|
||||||
|
suspend fun getWellKnown(): WellKnown?
|
||||||
|
suspend fun getElementWellKnown(): ElementWellKnown?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.wellknown.api
|
||||||
|
|
||||||
|
data class WellKnown(
|
||||||
|
val homeServer: WellKnownBaseConfig?,
|
||||||
|
val identityServer: WellKnownBaseConfig?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class WellKnownBaseConfig(
|
||||||
|
val baseURL: String?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.api
|
||||||
|
|
||||||
|
interface WellknownRetriever {
|
||||||
|
suspend fun getWellKnown(baseUrl: String): WellKnown?
|
||||||
|
suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown?
|
||||||
|
}
|
||||||
42
libraries/wellknown/impl/build.gradle.kts
Normal file
42
libraries/wellknown/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import extension.setupAnvil
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
alias(libs.plugins.kotlin.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.libraries.wellknown.impl"
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAnvil()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.libraries.wellknown.api)
|
||||||
|
implementation(libs.androidx.annotationjvm)
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(platform(libs.network.retrofit.bom))
|
||||||
|
implementation(libs.network.retrofit)
|
||||||
|
implementation(libs.serialization.json)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.network)
|
||||||
|
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
|
testImplementation(libs.coroutines.core)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.services.toolbox.test)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultSessionWellknownRetriever @Inject constructor(
|
||||||
|
private val matrixClient: MatrixClient,
|
||||||
|
private val parser: Json,
|
||||||
|
) : SessionWellknownRetriever {
|
||||||
|
private val domain by lazy { matrixClient.userIdServerName() }
|
||||||
|
|
||||||
|
override suspend fun getWellKnown(): WellKnown? {
|
||||||
|
val url = "https://$domain/.well-known/matrix/client"
|
||||||
|
return matrixClient
|
||||||
|
.getUrl(url)
|
||||||
|
.mapCatchingExceptions {
|
||||||
|
val data = String(it)
|
||||||
|
parser.decodeFromString(InternalWellKnown.serializer(), data)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") }
|
||||||
|
.map { it.map() }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getElementWellKnown(): ElementWellKnown? {
|
||||||
|
val url = "https://$domain/.well-known/element/element.json"
|
||||||
|
return matrixClient
|
||||||
|
.getUrl(url)
|
||||||
|
.mapCatchingExceptions {
|
||||||
|
val data = String(it)
|
||||||
|
parser.decodeFromString(InternalElementWellKnown.serializer(), data)
|
||||||
|
}
|
||||||
|
.onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") }
|
||||||
|
.map { it.map() }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.network.RetrofitFactory
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultWellknownRetriever @Inject constructor(
|
||||||
|
private val retrofitFactory: RetrofitFactory,
|
||||||
|
) : WellknownRetriever {
|
||||||
|
override suspend fun getWellKnown(baseUrl: String): WellKnown? {
|
||||||
|
val wellknownApi = buildWellknownApi(baseUrl) ?: return null
|
||||||
|
return try {
|
||||||
|
wellknownApi.getWellKnown().map()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to retrieve well-known data for $baseUrl")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? {
|
||||||
|
val wellknownApi = buildWellknownApi(baseUrl) ?: return null
|
||||||
|
return try {
|
||||||
|
wellknownApi.getElementWellKnown().map()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildWellknownApi(accountProviderUrl: String): WellknownAPI? {
|
||||||
|
return try {
|
||||||
|
retrofitFactory.create(accountProviderUrl.ensureProtocol())
|
||||||
|
.create(WellknownAPI::class.java)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If the base URL is not valid, we cannot retrieve the well-known data
|
||||||
|
Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2023, 2024 New Vector Ltd.
|
* Copyright 2025 New Vector Ltd.
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -20,9 +20,11 @@ import kotlinx.serialization.Serializable
|
||||||
* .
|
* .
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ElementWellKnown(
|
data class InternalElementWellKnown(
|
||||||
@SerialName("registration_helper_url")
|
@SerialName("registration_helper_url")
|
||||||
val registrationHelperUrl: String? = null,
|
val registrationHelperUrl: String? = null,
|
||||||
@SerialName("enforce_element_pro")
|
@SerialName("enforce_element_pro")
|
||||||
val enforceElementPro: Boolean? = null,
|
val enforceElementPro: Boolean? = null,
|
||||||
|
@SerialName("rageshake_url")
|
||||||
|
val rageshakeUrl: String? = null,
|
||||||
)
|
)
|
||||||
|
|
@ -5,9 +5,8 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
@ -26,13 +25,9 @@ import kotlinx.serialization.Serializable
|
||||||
* .
|
* .
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WellKnown(
|
data class InternalWellKnown(
|
||||||
@SerialName("m.homeserver")
|
@SerialName("m.homeserver")
|
||||||
val homeServer: WellKnownBaseConfig? = null,
|
val homeServer: InternalWellKnownBaseConfig? = null,
|
||||||
@SerialName("m.identity_server")
|
@SerialName("m.identity_server")
|
||||||
val identityServer: WellKnownBaseConfig? = null,
|
val identityServer: InternalWellKnownBaseConfig? = null,
|
||||||
) {
|
)
|
||||||
fun isValid(): Boolean {
|
|
||||||
return homeServer?.baseURL?.isNotBlank().orFalse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
@ -20,7 +20,7 @@ import kotlinx.serialization.Serializable
|
||||||
* .
|
* .
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class WellKnownBaseConfig(
|
data class InternalWellKnownBaseConfig(
|
||||||
@SerialName("base_url")
|
@SerialName("base_url")
|
||||||
val baseURL: String? = null
|
val baseURL: String? = null
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||||
|
|
||||||
|
internal fun InternalElementWellKnown.map() = ElementWellKnown(
|
||||||
|
registrationHelperUrl = registrationHelperUrl,
|
||||||
|
enforceElementPro = enforceElementPro,
|
||||||
|
rageshakeUrl = rageshakeUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun InternalWellKnown.map() = WellKnown(
|
||||||
|
homeServer = homeServer?.map(),
|
||||||
|
identityServer = identityServer?.map(),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun InternalWellKnownBaseConfig.map() = WellKnownBaseConfig(
|
||||||
|
baseURL = baseURL,
|
||||||
|
)
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.login.impl.resolver.network
|
package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
|
|
||||||
internal interface WellknownAPI {
|
internal interface WellknownAPI {
|
||||||
@GET(".well-known/matrix/client")
|
@GET(".well-known/matrix/client")
|
||||||
suspend fun getWellKnown(): WellKnown
|
suspend fun getWellKnown(): InternalWellKnown
|
||||||
|
|
||||||
@GET(".well-known/element/element.json")
|
@GET(".well-known/element/element.json")
|
||||||
suspend fun getElementWellKnown(): ElementWellKnown
|
suspend fun getElementWellKnown(): InternalElementWellKnown
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*
|
||||||
|
* 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.wellknown.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultSessionWellknownRetrieverTest {
|
||||||
|
@Test
|
||||||
|
fun `get empty wellknown`() = runTest {
|
||||||
|
val getUrlLambda = lambdaRecorder<String, Result<ByteArray>> {
|
||||||
|
Result.success("{}".toByteArray())
|
||||||
|
}
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = getUrlLambda,
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
|
WellKnown(
|
||||||
|
homeServer = null,
|
||||||
|
identityServer = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
getUrlLambda.assertions().isCalledOnce()
|
||||||
|
.with(value("https://user.domain.org/.well-known/matrix/client"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get wellknown with full content`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://example.org"
|
||||||
|
},
|
||||||
|
"m.identity_server": {
|
||||||
|
"base_url": "https://identity.example.org"
|
||||||
|
}
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
|
WellKnown(
|
||||||
|
homeServer = WellKnownBaseConfig(
|
||||||
|
baseURL = "https://example.org",
|
||||||
|
),
|
||||||
|
identityServer = WellKnownBaseConfig(
|
||||||
|
baseURL = "https://identity.example.org",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get wellknown with full content empty base_url`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://example.org"
|
||||||
|
},
|
||||||
|
"m.identity_server": {}
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
|
WellKnown(
|
||||||
|
homeServer = WellKnownBaseConfig(
|
||||||
|
baseURL = "https://example.org",
|
||||||
|
),
|
||||||
|
identityServer = WellKnownBaseConfig(
|
||||||
|
baseURL = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get wellknown with unknown key`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://example.org"
|
||||||
|
},
|
||||||
|
"m.identity_server": {
|
||||||
|
"base_url": "https://identity.example.org"
|
||||||
|
},
|
||||||
|
"other": true
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
|
WellKnown(
|
||||||
|
homeServer = WellKnownBaseConfig(
|
||||||
|
baseURL = "https://example.org",
|
||||||
|
),
|
||||||
|
identityServer = WellKnownBaseConfig(
|
||||||
|
baseURL = "https://identity.example.org",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get wellknown json error`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://example.org"
|
||||||
|
},
|
||||||
|
error
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get wellknown network error`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getWellKnown()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get empty element wellknown`() = runTest {
|
||||||
|
val getUrlLambda = lambdaRecorder<String, Result<ByteArray>> {
|
||||||
|
Result.success("{}".toByteArray())
|
||||||
|
}
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = getUrlLambda,
|
||||||
|
)
|
||||||
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
|
ElementWellKnown(
|
||||||
|
registrationHelperUrl = null,
|
||||||
|
enforceElementPro = null,
|
||||||
|
rageshakeUrl = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
getUrlLambda.assertions().isCalledOnce()
|
||||||
|
.with(value("https://user.domain.org/.well-known/element/element.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown with full content`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"registration_helper_url": "a_registration_url",
|
||||||
|
"enforce_element_pro": true,
|
||||||
|
"rageshake_url": "a_rageshake_url"
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
|
ElementWellKnown(
|
||||||
|
registrationHelperUrl = "a_registration_url",
|
||||||
|
enforceElementPro = true,
|
||||||
|
rageshakeUrl = "a_rageshake_url",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown with unknown key`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"registration_helper_url": "a_registration_url",
|
||||||
|
"enforce_element_pro": true,
|
||||||
|
"rageshake_url": "a_rageshake_url",
|
||||||
|
"other": true
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
|
ElementWellKnown(
|
||||||
|
registrationHelperUrl = "a_registration_url",
|
||||||
|
enforceElementPro = true,
|
||||||
|
rageshakeUrl = "a_rageshake_url",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown json error`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.success(
|
||||||
|
"""{
|
||||||
|
"registration_helper_url" = "a_registration_url",
|
||||||
|
error
|
||||||
|
}""".trimIndent().toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getElementWellKnown()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `get element wellknown network error`() = runTest {
|
||||||
|
val sut = createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda = {
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assertThat(sut.getElementWellKnown()).isNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDefaultSessionWellknownRetriever(
|
||||||
|
getUrlLambda: (String) -> Result<ByteArray>,
|
||||||
|
) = DefaultSessionWellknownRetriever(
|
||||||
|
matrixClient = FakeMatrixClient(
|
||||||
|
userIdServerNameLambda = { "user.domain.org" },
|
||||||
|
getUrlLambda = getUrlLambda,
|
||||||
|
),
|
||||||
|
parser = Json { ignoreUnknownKeys = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
19
libraries/wellknown/test/build.gradle.kts
Normal file
19
libraries/wellknown/test/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.wellknown.test"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.wellknown.api)
|
||||||
|
implementation(projects.tests.testutils)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.features.wellknown.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeSessionWellknownRetriever(
|
||||||
|
private val getWellKnownResult: () -> WellKnown? = { null },
|
||||||
|
private val getElementWellKnownResult: () -> ElementWellKnown? = { null },
|
||||||
|
) : SessionWellknownRetriever {
|
||||||
|
override suspend fun getWellKnown(): WellKnown? = simulateLongTask {
|
||||||
|
getWellKnownResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getElementWellKnown(): ElementWellKnown? = simulateLongTask {
|
||||||
|
getElementWellKnownResult()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* 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.features.wellknown.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeWellknownRetriever(
|
||||||
|
private val getWellKnownResult: (String) -> WellKnown? = { null },
|
||||||
|
private val getElementWellKnownResult: (String) -> ElementWellKnown? = { null },
|
||||||
|
) : WellknownRetriever {
|
||||||
|
override suspend fun getWellKnown(baseUrl: String): WellKnown? = simulateLongTask {
|
||||||
|
getWellKnownResult(baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? = simulateLongTask {
|
||||||
|
getElementWellKnownResult(baseUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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.features.wellknown.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
|
||||||
|
fun anElementWellKnown(
|
||||||
|
registrationHelperUrl: String? = null,
|
||||||
|
enforceElementPro: Boolean? = null,
|
||||||
|
rageshakeUrl: String? = null,
|
||||||
|
) = ElementWellKnown(
|
||||||
|
registrationHelperUrl = registrationHelperUrl,
|
||||||
|
enforceElementPro = enforceElementPro,
|
||||||
|
rageshakeUrl = rageshakeUrl,
|
||||||
|
)
|
||||||
|
|
@ -89,6 +89,7 @@ fun DependencyHandlerScope.allLibrariesImpl() {
|
||||||
implementation(project(":libraries:mediaviewer:impl"))
|
implementation(project(":libraries:mediaviewer:impl"))
|
||||||
implementation(project(":libraries:troubleshoot:impl"))
|
implementation(project(":libraries:troubleshoot:impl"))
|
||||||
implementation(project(":libraries:fullscreenintent:impl"))
|
implementation(project(":libraries:fullscreenintent:impl"))
|
||||||
|
implementation(project(":libraries:wellknown:impl"))
|
||||||
implementation(project(":libraries:oidc:impl"))
|
implementation(project(":libraries:oidc:impl"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,4 +68,30 @@ class KonsistLicenseTest {
|
||||||
enterpriseLicense.containsMatchIn(it.text)
|
enterpriseLicense.containsMatchIn(it.text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `assert that files do not have double license header`() {
|
||||||
|
Konsist
|
||||||
|
.scopeFromProject()
|
||||||
|
.files
|
||||||
|
.filter {
|
||||||
|
it.nameWithExtension != "locales.kt" &&
|
||||||
|
it.nameWithExtension != "KonsistLicenseTest.kt" &&
|
||||||
|
it.name.startsWith("Template ").not()
|
||||||
|
}
|
||||||
|
.assertTrue {
|
||||||
|
it.text.count("New Vector") == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.count(subString: String): Int {
|
||||||
|
var count = 0
|
||||||
|
var index = 0
|
||||||
|
while (true) {
|
||||||
|
index = indexOf(subString, index)
|
||||||
|
if (index == -1) return count
|
||||||
|
count++
|
||||||
|
index += subString.length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue