Merge pull request #4285 from element-hq/feature/bma/appConfig

Prepare application for being configurable
This commit is contained in:
Benoit Marty 2025-02-21 16:43:06 +01:00 committed by GitHub
commit 23bc71f331
21 changed files with 303 additions and 171 deletions

View file

@ -10,6 +10,7 @@
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
import com.android.build.gradle.internal.tasks.factory.dependsOn import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.android.build.gradle.tasks.GenerateBuildConfig import com.android.build.gradle.tasks.GenerateBuildConfig
import config.BuildTimeConfig
import extension.AssetCopyTask import extension.AssetCopyTask
import extension.ComponentMergingStrategy import extension.ComponentMergingStrategy
import extension.GitBranchNameValueSource import extension.GitBranchNameValueSource
@ -43,11 +44,7 @@ android {
namespace = "io.element.android.x" namespace = "io.element.android.x"
defaultConfig { defaultConfig {
applicationId = if (isEnterpriseBuild) { applicationId = BuildTimeConfig.APPLICATION_ID
"io.element.enterprise"
} else {
"io.element.android.x"
}
targetSdk = Versions.TARGET_SDK targetSdk = Versions.TARGET_SDK
versionCode = Versions.VERSION_CODE versionCode = Versions.VERSION_CODE
versionName = Versions.VERSION_NAME versionName = Versions.VERSION_NAME
@ -97,11 +94,7 @@ android {
} }
} }
val baseAppName = if (isEnterpriseBuild) { val baseAppName = BuildTimeConfig.APPLICATION_NAME
"Element Enterprise"
} else {
"Element X"
}
logger.warnInBox("Building $baseAppName") logger.warnInBox("Building $baseAppName")
buildTypes { buildTypes {

View file

@ -73,23 +73,26 @@ object AppModule {
@ApplicationContext context: Context, @ApplicationContext context: Context,
buildType: BuildType, buildType: BuildType,
enterpriseService: EnterpriseService, enterpriseService: EnterpriseService,
) = BuildMeta( ): BuildMeta {
isDebuggable = BuildConfig.DEBUG, val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
buildType = buildType, return BuildMeta(
applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name), isDebuggable = BuildConfig.DEBUG,
productionApplicationName = ApplicationConfig.PRODUCTION_APPLICATION_NAME, buildType = buildType,
desktopApplicationName = ApplicationConfig.DESKTOP_APPLICATION_NAME, applicationName = applicationName,
applicationId = BuildConfig.APPLICATION_ID, productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
isEnterpriseBuild = enterpriseService.isEnterpriseBuild, desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE, applicationId = BuildConfig.APPLICATION_ID,
lowPrivacyLoggingEnabled = false, isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
versionName = BuildConfig.VERSION_NAME, // TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
versionCode = context.getVersionCodeFromManifest(), lowPrivacyLoggingEnabled = false,
gitRevision = BuildConfig.GIT_REVISION, versionName = BuildConfig.VERSION_NAME,
gitBranchName = BuildConfig.GIT_BRANCH_NAME, versionCode = context.getVersionCodeFromManifest(),
flavorDescription = BuildConfig.FLAVOR_DESCRIPTION, gitRevision = BuildConfig.GIT_REVISION,
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION, gitBranchName = BuildConfig.GIT_BRANCH_NAME,
) flavorDescription = BuildConfig.FLAVOR_DESCRIPTION,
flavorShortDescription = BuildConfig.SHORT_FLAVOR_DESCRIPTION,
)
}
@Provides @Provides
@SingleIn(AppScope::class) @SingleIn(AppScope::class)

View file

@ -10,11 +10,6 @@ package io.element.android.appconfig
object AuthenticationConfig { object AuthenticationConfig {
const val MATRIX_ORG_URL = "https://matrix.org" const val MATRIX_ORG_URL = "https://matrix.org"
/**
* Default homeserver url to sign in with, unless the user selects a different one.
*/
const val DEFAULT_HOMESERVER_URL = MATRIX_ORG_URL
/** /**
* URL with some docs that explain what's sliding sync and how to add it to your home server. * URL with some docs that explain what's sliding sync and how to add it to your home server.
*/ */

@ -1 +1 @@
Subproject commit 0c028db8a48118433c7e11737080a6a01fb90f69 Subproject commit 6d96bf58aec2ecc77b408858272cd64ec26e10d0

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.api.core.SessionId
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
fun defaultHomeserver(): String?
fun semanticColorsLight(): SemanticColors fun semanticColorsLight(): SemanticColors
fun semanticColorsDark(): SemanticColors fun semanticColorsDark(): SemanticColors

View file

@ -22,6 +22,8 @@ class DefaultEnterpriseService @Inject constructor() : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override fun defaultHomeserver() = null
override fun semanticColorsLight(): SemanticColors = compoundColorsLight override fun semanticColorsLight(): SemanticColors = compoundColorsLight
override fun semanticColorsDark(): SemanticColors = compoundColorsDark override fun semanticColorsDark(): SemanticColors = compoundColorsDark

View file

@ -19,6 +19,12 @@ class DefaultEnterpriseServiceTest {
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse() assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
} }
@Test
fun `defaultHomeserver should return null`() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat<String?>(defaultEnterpriseService.defaultHomeserver()).isNull()
}
@Test @Test
fun `isEnterpriseUser always return false`() = runTest { fun `isEnterpriseUser always return false`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()

View file

@ -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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.enterprise.test"
}
dependencies {
api(projects.features.enterprise.api)
implementation(libs.compound)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
}

View file

@ -0,0 +1,42 @@
/*
* 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.test
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverResult: () -> String? = { A_FAKE_HOMESERVER },
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
) : EnterpriseService {
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
isEnterpriseUserResult(sessionId)
}
override fun defaultHomeserver(): String? {
return defaultHomeserverResult()
}
override fun semanticColorsLight(): SemanticColors {
return semanticColorsLightResult()
}
override fun semanticColorsDark(): SemanticColors {
return semanticColorsDarkResult()
}
companion object {
const val A_FAKE_HOMESERVER = "a_fake_homeserver"
}
}

View file

@ -28,6 +28,7 @@ setupAnvil(componentMergingStrategy = ComponentMergingStrategy.KSP)
dependencies { dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
@ -55,6 +56,7 @@ dependencies {
testImplementation(libs.test.robolectric) testImplementation(libs.test.robolectric)
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.oidc.impl) testImplementation(projects.libraries.oidc.impl)
testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.permissions.test)

View file

@ -7,7 +7,8 @@
package io.element.android.features.login.impl.accountprovider package io.element.android.features.login.impl.accountprovider
import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.appconfig.AuthenticationConfig
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.di.SingleIn import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -16,7 +17,18 @@ import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject import javax.inject.Inject
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
class AccountProviderDataSource @Inject constructor() { class AccountProviderDataSource @Inject constructor(
enterpriseService: EnterpriseService,
) {
private val defaultAccountProvider = (enterpriseService.defaultHomeserver() ?: AuthenticationConfig.MATRIX_ORG_URL).let { url ->
AccountProvider(
url = url,
subtitle = null,
isPublic = url == AuthenticationConfig.MATRIX_ORG_URL,
isMatrixOrg = url == AuthenticationConfig.MATRIX_ORG_URL,
)
}
private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow( private val accountProvider: MutableStateFlow<AccountProvider> = MutableStateFlow(
defaultAccountProvider defaultAccountProvider
) )

View file

@ -1,18 +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.util
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.login.impl.accountprovider.AccountProvider
val defaultAccountProvider = AccountProvider(
url = AuthenticationConfig.DEFAULT_HOMESERVER_URL,
subtitle = null,
isPublic = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL,
isMatrixOrg = AuthenticationConfig.DEFAULT_HOMESERVER_URL == AuthenticationConfig.MATRIX_ORG_URL,
)

View file

@ -0,0 +1,84 @@
/*
* 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.accountprovider
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AuthenticationConfig
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class AccountProviderDataSourceTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())
sut.flow().test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
AccountProvider(
url = FakeEnterpriseService.A_FAKE_HOMESERVER,
title = FakeEnterpriseService.A_FAKE_HOMESERVER,
subtitle = null,
isPublic = false,
isMatrixOrg = false,
isValid = false,
)
)
}
}
@Test
fun `present - initial state - matrix org`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService(
defaultHomeserverResult = { AuthenticationConfig.MATRIX_ORG_URL }
))
sut.flow().test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(
AccountProvider(
url = AuthenticationConfig.MATRIX_ORG_URL,
title = "matrix.org",
subtitle = null,
isPublic = true,
isMatrixOrg = true,
isValid = false,
)
)
}
}
@Test
fun `present - user change and reset`() = runTest {
val sut = AccountProviderDataSource(FakeEnterpriseService())
sut.flow().test {
val initialState = awaitItem()
assertThat(initialState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
sut.userSelection(AccountProvider(url = "https://example.com"))
val changedState = awaitItem()
assertThat(changedState).isEqualTo(
AccountProvider(
url = "https://example.com",
title = "example.com",
subtitle = null,
isPublic = false,
isMatrixOrg = false,
isValid = false,
)
)
sut.reset()
val resetState = awaitItem()
assertThat(resetState.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
}
}
}

View file

@ -7,10 +7,8 @@
package io.element.android.features.login.impl.changeserver package io.element.android.features.login.impl.changeserver
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.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.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -18,6 +16,7 @@ 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.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
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
@ -28,13 +27,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val presenter = ChangeServerPresenter( createPresenter().test {
FakeMatrixAuthenticationService(),
AccountProviderDataSource()
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
} }
@ -43,13 +36,9 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server ok`() = runTest { fun `present - change server ok`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val presenter = ChangeServerPresenter( createPresenter(
authenticationService, authenticationService = authenticationService,
AccountProviderDataSource() ).test {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
@ -63,14 +52,7 @@ class ChangeServerPresenterTest {
@Test @Test
fun `present - change server error`() = runTest { fun `present - change server error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() createPresenter().test {
val presenter = ChangeServerPresenter(
authenticationService,
AccountProviderDataSource()
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL)))
@ -84,4 +66,12 @@ class ChangeServerPresenterTest {
assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized) assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
private fun createPresenter(
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
) = ChangeServerPresenter(
authenticationService = authenticationService,
accountProviderDataSource = accountProviderDataSource
)
} }

View file

@ -11,10 +11,10 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.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.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
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.screens.createaccount.AccountCreationNotSupported import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported
import io.element.android.features.login.impl.util.defaultAccountProvider
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.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -44,7 +44,7 @@ class ConfirmAccountProviderPresenterTest {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.isAccountCreation).isFalse()
assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.submitEnabled).isTrue()
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) assertThat(initialState.accountProvider.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.loginFlow).isEqualTo(AsyncData.Uninitialized)
} }
} }
@ -350,7 +350,7 @@ class ConfirmAccountProviderPresenterTest {
private fun createConfirmAccountProviderPresenter( private fun createConfirmAccountProviderPresenter(
params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false), params: ConfirmAccountProviderPresenter.Params = ConfirmAccountProviderPresenter.Params(isAccountCreation = false),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(), defaultOidcActionFlow: DefaultOidcActionFlow = DefaultOidcActionFlow(),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(), defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory(),

View file

@ -8,8 +8,8 @@
package io.element.android.features.login.impl.screens.createaccount package io.element.android.features.login.impl.screens.createaccount
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.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.matrix.api.auth.external.ExternalSession import io.element.android.libraries.matrix.api.auth.external.ExternalSession
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import org.junit.Assert.assertThrows import org.junit.Assert.assertThrows
@ -27,9 +27,7 @@ class DefaultMessageParserTest {
@Test @Test
fun `DefaultMessageParser is able to parse correct message`() { fun `DefaultMessageParser is able to parse correct message`() {
val sut = DefaultMessageParser( val sut = createDefaultMessageParser()
AccountProviderDataSource()
)
assertThat(sut.parse(validMessage)).isEqualTo( assertThat(sut.parse(validMessage)).isEqualTo(
anExternalSession( anExternalSession(
homeserverUrl = "home_server", homeserverUrl = "home_server",
@ -39,9 +37,7 @@ class DefaultMessageParserTest {
@Test @Test
fun `DefaultMessageParser should throw Exception in case of error`() { fun `DefaultMessageParser should throw Exception in case of error`() {
val sut = DefaultMessageParser( val sut = createDefaultMessageParser()
AccountProviderDataSource()
)
// kotlinx.serialization.json.internal.JsonDecodingException // kotlinx.serialization.json.internal.JsonDecodingException
assertThrows(SerializationException::class.java) { sut.parse("invalid json") } assertThrows(SerializationException::class.java) { sut.parse("invalid json") }
// missing userId // missing userId
@ -60,16 +56,20 @@ class DefaultMessageParserTest {
@Test @Test
fun `DefaultMessageParser should be successful even is homeserver url is missing`() { fun `DefaultMessageParser should be successful even is homeserver url is missing`() {
val sut = DefaultMessageParser( val sut = createDefaultMessageParser()
AccountProviderDataSource()
)
// missing homeServer // missing homeServer
assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo( assertThat(sut.parse(validMessage.replace(""""home_server": "home_server",""", ""))).isEqualTo(
anExternalSession( anExternalSession(
homeserverUrl = defaultAccountProvider.url, homeserverUrl = FakeEnterpriseService.A_FAKE_HOMESERVER,
) )
) )
} }
private fun createDefaultMessageParser(): DefaultMessageParser {
return DefaultMessageParser(
AccountProviderDataSource(FakeEnterpriseService())
)
}
} }
internal fun anExternalSession( internal fun anExternalSession(

View file

@ -7,13 +7,10 @@
package io.element.android.features.login.impl.screens.loginpassword package io.element.android.features.login.impl.screens.loginpassword
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
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.login.impl.DefaultLoginUserStory import io.element.android.features.login.impl.DefaultLoginUserStory
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.util.defaultAccountProvider
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_HOMESERVER import io.element.android.libraries.matrix.test.A_HOMESERVER
@ -23,6 +20,7 @@ import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.test
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
@ -33,19 +31,9 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - initial state`() = runTest { fun `present - initial state`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() createLoginPasswordPresenter().test {
val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter(
authenticationService,
accountProviderDataSource,
loginUserStory,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) assertThat(initialState.accountProvider.url).isEqualTo(FakeEnterpriseService.A_FAKE_HOMESERVER)
assertThat(initialState.formState).isEqualTo(LoginFormState.Default) assertThat(initialState.formState).isEqualTo(LoginFormState.Default)
assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized) assertThat(initialState.loginAction).isEqualTo(AsyncData.Uninitialized)
assertThat(initialState.submitEnabled).isFalse() assertThat(initialState.submitEnabled).isFalse()
@ -55,17 +43,10 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - enter login and password`() = runTest { fun `present - enter login and password`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter(
authenticationService,
accountProviderDataSource,
loginUserStory,
)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionMode.Immediate) { createLoginPasswordPresenter(
presenter.present() authenticationService = authenticationService,
}.test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
val loginState = awaitItem() val loginState = awaitItem()
@ -81,17 +62,12 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit`() = runTest { fun `present - submit`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
val presenter = LoginPasswordPresenter(
authenticationService,
accountProviderDataSource,
loginUserStory,
)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionMode.Immediate) { val loginUserStory = DefaultLoginUserStory().apply { setLoginFlowIsDone(false) }
presenter.present() createLoginPasswordPresenter(
}.test { authenticationService = authenticationService,
defaultLoginUserStory = loginUserStory,
).test {
assertThat(loginUserStory.loginFlowIsDone.value).isFalse() assertThat(loginUserStory.loginFlowIsDone.value).isFalse()
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
@ -110,17 +86,10 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - submit with error`() = runTest { fun `present - submit with error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter(
authenticationService,
accountProviderDataSource,
loginUserStory,
)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionMode.Immediate) { createLoginPasswordPresenter(
presenter.present() authenticationService = authenticationService,
}.test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
@ -138,17 +107,10 @@ class LoginPasswordPresenterTest {
@Test @Test
fun `present - clear error`() = runTest { fun `present - clear error`() = runTest {
val authenticationService = FakeMatrixAuthenticationService() val authenticationService = FakeMatrixAuthenticationService()
val accountProviderDataSource = AccountProviderDataSource()
val loginUserStory = DefaultLoginUserStory()
val presenter = LoginPasswordPresenter(
authenticationService,
accountProviderDataSource,
loginUserStory,
)
authenticationService.givenHomeserver(A_HOMESERVER) authenticationService.givenHomeserver(A_HOMESERVER)
moleculeFlow(RecompositionMode.Immediate) { createLoginPasswordPresenter(
presenter.present() authenticationService = authenticationService,
}.test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME))
initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD))
@ -167,4 +129,14 @@ class LoginPasswordPresenterTest {
assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized) assertThat(clearedState.loginAction).isEqualTo(AsyncData.Uninitialized)
} }
} }
private fun createLoginPasswordPresenter(
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
defaultLoginUserStory: DefaultLoginUserStory = DefaultLoginUserStory()
): LoginPasswordPresenter = LoginPasswordPresenter(
authenticationService = authenticationService,
accountProviderDataSource = accountProviderDataSource,
defaultLoginUserStory = defaultLoginUserStory,
)
} }

View file

@ -7,6 +7,7 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import config.BuildTimeConfig
import extension.setupAnvil import extension.setupAnvil
plugins { plugins {
@ -22,22 +23,14 @@ android {
resValue( resValue(
type = "string", type = "string",
name = "google_app_id", name = "google_app_id",
value = if (isEnterpriseBuild) { value = BuildTimeConfig.GOOGLE_APP_ID_RELEASE,
"1:912726360885:android:d273c2077ec3291500427c"
} else {
"1:912726360885:android:d097de99a4c23d2700427c"
}
) )
} }
getByName("debug") { getByName("debug") {
resValue( resValue(
type = "string", type = "string",
name = "google_app_id", name = "google_app_id",
value = if (isEnterpriseBuild) { value = BuildTimeConfig.GOOGLE_APP_ID_DEBUG,
"1:912726360885:android:f8de9126a94143d300427c"
} else {
"1:912726360885:android:def0a4e454042e9b00427c"
}
) )
} }
register("nightly") { register("nightly") {
@ -46,11 +39,7 @@ android {
resValue( resValue(
type = "string", type = "string",
name = "google_app_id", name = "google_app_id",
value = if (isEnterpriseBuild) { value = BuildTimeConfig.GOOGLE_APP_ID_NIGHTLY,
"1:912726360885:android:3f7e1fe644d99d5a00427c"
} else {
"1:912726360885:android:e17435e0beb0303000427c"
}
) )
} }
} }

View file

@ -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 config
object BuildTimeConfig {
const val APPLICATION_ID = "io.element.android.x"
const val APPLICATION_NAME = "Element X"
const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c"
const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
}

View file

@ -25,7 +25,7 @@ internal fun DependencyHandler.implementation(dependency: Any) = add("implementa
private fun DependencyHandlerScope.implementation( private fun DependencyHandlerScope.implementation(
dependency: Any, dependency: Any,
config: Action<ExternalModuleDependency> config: Action<ExternalModuleDependency>
) = dependencies.add("implementation", dependency, closureOf<ExternalModuleDependency> { config.execute(this) }) ) = dependencies.add("implementation", dependency, closureOf<ExternalModuleDependency> { config.execute(this) })
private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency) private fun DependencyHandlerScope.androidTestImplementation(dependency: Any) = dependencies.add("androidTestImplementation", dependency)
@ -114,14 +114,30 @@ fun DependencyHandlerScope.allServicesImpl() {
implementation(project(":services:toolbox:impl")) implementation(project(":services:toolbox:impl"))
} }
fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(project, "enterprise", "impl") fun DependencyHandlerScope.allEnterpriseImpl(project: Project) = addAll(
project = project,
modulePrefix = ":enterprise:features",
moduleSuffix = ":impl",
)
fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(project, "features", "impl") fun DependencyHandlerScope.allFeaturesImpl(project: Project) = addAll(
project = project,
modulePrefix = ":features",
moduleSuffix = ":impl",
)
fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(project, "features", "api") fun DependencyHandlerScope.allFeaturesApi(project: Project) = addAll(
project = project,
modulePrefix = ":features",
moduleSuffix = ":api",
)
private fun DependencyHandlerScope.addAll(project: Project, prefix: String, suffix: String) { private fun DependencyHandlerScope.addAll(
val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(":$prefix") && it.path.endsWith(":$suffix") } project: Project,
modulePrefix: String,
moduleSuffix: String,
) {
val subProjects = project.rootProject.subprojects.filter { it.path.startsWith(modulePrefix) && it.path.endsWith(moduleSuffix) }
for (p in subProjects) { for (p in subProjects) {
add("implementation", p) add("implementation", p)
} }

View file

@ -7,6 +7,7 @@
package io.element.android.tests.konsist package io.element.android.tests.konsist
import com.google.common.truth.Truth.assertThat
import com.lemonappdev.konsist.api.Konsist import com.lemonappdev.konsist.api.Konsist
import com.lemonappdev.konsist.api.verify.assertTrue import com.lemonappdev.konsist.api.verify.assertTrue
import org.junit.Test import org.junit.Test
@ -43,10 +44,13 @@ class KonsistLicenseTest {
.scopeFromProject() .scopeFromProject()
.files .files
.filter { .filter {
it.path.contains("/enterprise/features").not() && it.moduleName.startsWith("enterprise").not() &&
it.nameWithExtension != "locales.kt" && it.nameWithExtension != "locales.kt" &&
it.name.startsWith("Template ").not() it.name.startsWith("Template ").not()
} }
.also {
assertThat(it).isNotEmpty()
}
.assertTrue { .assertTrue {
publicLicense.containsMatchIn(it.text) publicLicense.containsMatchIn(it.text)
} }
@ -58,7 +62,10 @@ class KonsistLicenseTest {
.scopeFromProject() .scopeFromProject()
.files .files
.filter { .filter {
it.path.contains("/enterprise/features") it.moduleName.startsWith("enterprise")
}
.also {
assertThat(it).isNotEmpty()
} }
.assertTrue { .assertTrue {
enterpriseLicense.containsMatchIn(it.text) enterpriseLicense.containsMatchIn(it.text)