feat: Configuration module improvements.
This commit is contained in:
parent
b3f95c2594
commit
0d7b5f9d8f
|
@ -22,10 +22,13 @@ import dagger.Module
|
|||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.configurator.domain.ConfigurationUseCase
|
||||
import me.proton.core.configuration.configurator.domain.EnvironmentConfigurationUseCase
|
||||
import me.proton.core.configuration.configurator.entity.AppConfig
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Singleton
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Module
|
||||
|
@ -37,8 +40,8 @@ object ApplicationModule {
|
|||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
val clientTimeout = 3.seconds.toJavaDuration()
|
||||
fun provideOkHttpClient(appConfig: AppConfig): OkHttpClient {
|
||||
val clientTimeout = appConfig.quarkTimeout.toJavaDuration()
|
||||
return OkHttpClient.Builder().connectTimeout(clientTimeout)
|
||||
.readTimeout(clientTimeout)
|
||||
.writeTimeout(clientTimeout)
|
||||
|
@ -46,4 +49,12 @@ object ApplicationModule {
|
|||
.retryOnConnectionFailure(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideContentResolverConfigurationUseCase(
|
||||
contentResolverConfigManager: ContentResolverConfigManager,
|
||||
appConfig: AppConfig,
|
||||
quark: QuarkCommand
|
||||
): ConfigurationUseCase = EnvironmentConfigurationUseCase(quark, contentResolverConfigManager, appConfig)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.configurator.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.core.configuration.configurator.entity.AppConfig
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ConfigurationModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAppConfig(): AppConfig = AppConfig()
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.configurator.domain
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.configurator.entity.Configuration
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
typealias ConfigFieldSet = Set<ConfigurationUseCase.ConfigField>
|
||||
|
||||
open class ConfigurationUseCase(
|
||||
private val contentResolverConfigManager: ContentResolverConfigManager,
|
||||
private val configClass: KClass<*>,
|
||||
private val defaultConfigValueMapper: (ConfigFieldSet, Map<String, Any?>) -> ConfigFieldSet,
|
||||
private val supportedContractFieldSet: ConfigFieldSet,
|
||||
) : Configuration {
|
||||
|
||||
data class ConfigField(
|
||||
val name: String,
|
||||
val isAdvanced: Boolean = true,
|
||||
val isPreserved: Boolean = false,
|
||||
val value: Any? = "",
|
||||
val fetcher: (suspend (String) -> Any)? = null,
|
||||
)
|
||||
|
||||
private val _configState = MutableStateFlow(supportedContractFieldSet)
|
||||
|
||||
var configState: StateFlow<ConfigFieldSet> = _configState.asStateFlow()
|
||||
|
||||
fun setDefaultConfigurationFields() {
|
||||
val newValueMap = _configState.value.filter { it.isPreserved }.associate { it.name to it.value }
|
||||
_configState.value = defaultConfigValueMapper(_configState.value, newValueMap).toSet()
|
||||
}
|
||||
|
||||
override suspend fun fetchConfig() {
|
||||
val resolvedConfigMap = contentResolverConfigManager.queryAtClassPath(configClass)
|
||||
|
||||
_configState.value =
|
||||
if (resolvedConfigMap == null) {
|
||||
supportedContractFieldSet
|
||||
} else {
|
||||
defaultConfigValueMapper(_configState.value, resolvedConfigMap)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun saveConfig(advanced: Boolean) {
|
||||
val stateToInsert = _configState.value.filter { if (advanced) true else !it.isAdvanced }
|
||||
val mapToInsert = stateToInsert.associate { it.name to it.value }
|
||||
contentResolverConfigManager.insertConfigFieldMapAtClassPath(mapToInsert, configClass)
|
||||
}
|
||||
|
||||
override suspend fun updateConfigField(key: String, newValue: Any) {
|
||||
_configState.value = _configState.value.withUpdatedValues(key, newValue)
|
||||
}
|
||||
|
||||
override suspend fun fetchConfigField(key: String) {
|
||||
updateConfigField(
|
||||
key,
|
||||
supportedContractFieldSet.firstOrNull { it.name == key }?.fetcher?.let { it(key) }.toString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ConfigFieldSet.withUpdatedValues(key: String, newValue: Any): ConfigFieldSet =
|
||||
map { if (it.name == key) it.copy(value = newValue) else it }.toSet()
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.configurator.domain
|
||||
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.EnvironmentConfiguration
|
||||
import me.proton.core.configuration.configurator.entity.AppConfig
|
||||
import me.proton.core.configuration.configurator.extension.getProxyToken
|
||||
import me.proton.core.configuration.entity.ConfigContract
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import javax.inject.Inject
|
||||
|
||||
class EnvironmentConfigurationUseCase @Inject constructor(
|
||||
quark: QuarkCommand,
|
||||
contentResolverConfigManager: ContentResolverConfigManager,
|
||||
appConfig: AppConfig
|
||||
) : ConfigurationUseCase(
|
||||
contentResolverConfigManager = contentResolverConfigManager,
|
||||
configClass = EnvironmentConfiguration::class,
|
||||
supportedContractFieldSet = setOf(
|
||||
ConfigField(ConfigContract::host.name, isAdvanced = false, isPreserved = true, value = defaultConfig.host),
|
||||
ConfigField(ConfigContract::proxyToken.name, isAdvanced = false, isPreserved = true) {
|
||||
quark.baseUrl(appConfig.proxyUrl).getProxyToken() ?: error("Could not obtain proxy token")
|
||||
},
|
||||
ConfigField(ConfigContract::apiPrefix.name, isPreserved = true, value = defaultConfig.apiPrefix),
|
||||
ConfigField(ConfigContract::apiHost.name, value = defaultConfig.apiHost),
|
||||
ConfigField(ConfigContract::baseUrl.name, value = defaultConfig.baseUrl),
|
||||
ConfigField(ConfigContract::hv3Host.name, value = defaultConfig.hv3Host),
|
||||
ConfigField(ConfigContract::hv3Url.name, value = defaultConfig.hv3Url),
|
||||
ConfigField(ConfigContract::useDefaultPins.name, value = false),
|
||||
),
|
||||
defaultConfigValueMapper = ::configFieldMapper
|
||||
) {
|
||||
companion object {
|
||||
fun configFieldMapper(configFieldSet: ConfigFieldSet, configFieldMap: Map<String, Any?>): ConfigFieldSet {
|
||||
val config = EnvironmentConfiguration.fromMap(configFieldMap).primitiveFieldMap
|
||||
return configFieldSet.map { it.copy(value = config[it.name]) }.toSet()
|
||||
}
|
||||
|
||||
val defaultConfig: EnvironmentConfiguration =
|
||||
EnvironmentConfiguration.fromMap(mapOf("host" to "proton.black"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.configurator.entity
|
||||
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class AppConfig(
|
||||
val quarkTimeout: Duration = 3.seconds,
|
||||
val proxyUrl: String = "https://proxy.proton.black"
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.configurator.entity
|
||||
|
||||
interface Configuration {
|
||||
suspend fun fetchConfig()
|
||||
suspend fun saveConfig(advanced: Boolean)
|
||||
suspend fun updateConfigField(key: String, newValue: Any)
|
||||
suspend fun fetchConfigField(key: String)
|
||||
}
|
|
@ -27,7 +27,6 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -35,83 +34,33 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import me.proton.core.compose.component.ProtonSnackbarHost
|
||||
import me.proton.core.compose.component.ProtonSnackbarHostState
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.EnvironmentConfiguration
|
||||
import me.proton.core.configuration.configurator.BuildConfig
|
||||
import me.proton.core.configuration.configurator.R
|
||||
import me.proton.core.configuration.configurator.extension.getProxyToken
|
||||
import me.proton.core.configuration.configurator.presentation.components.ConfigurationScreen
|
||||
import me.proton.core.configuration.configurator.presentation.components.FieldActionMap
|
||||
import me.proton.core.configuration.configurator.presentation.viewModel.ConfigurationScreenViewModel
|
||||
import me.proton.core.configuration.entity.ConfigContract
|
||||
import me.proton.core.presentation.ui.ProtonActivity
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ConfigurationActivity : ProtonActivity() {
|
||||
|
||||
@Inject
|
||||
lateinit var quark: QuarkCommand
|
||||
|
||||
@Inject
|
||||
lateinit var contentResolverConfigManager: ContentResolverConfigManager
|
||||
|
||||
private val basicEnvConfigFields: FieldActionMap = mapOf(
|
||||
ConfigContract::host.name to null,
|
||||
ConfigContract::proxyToken.name to {
|
||||
quark.baseUrl(BuildConfig.PROXY_URL).getProxyToken() ?: error("Could not obtain proxy token")
|
||||
},
|
||||
)
|
||||
|
||||
private val advancedEnvConfigFields: FieldActionMap = basicEnvConfigFields + mapOf(
|
||||
ConfigContract::apiHost.name to null,
|
||||
ConfigContract::apiPrefix.name to null,
|
||||
ConfigContract::baseUrl.name to null,
|
||||
ConfigContract::hv3Host.name to null,
|
||||
ConfigContract::hv3Url.name to null,
|
||||
ConfigContract::useDefaultPins.name to null,
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val snackbarHostState = remember { ProtonSnackbarHostState() }
|
||||
Box {
|
||||
ProtonTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column {
|
||||
ConfigurationScreen(
|
||||
configViewModel = ConfigurationScreenViewModel(
|
||||
contentResolverConfigManager = contentResolverConfigManager,
|
||||
configFieldMapper = EnvironmentConfiguration::fromMap,
|
||||
defaultConfig = EnvironmentConfiguration.fromMap(mapOf())
|
||||
),
|
||||
basicFields = basicEnvConfigFields,
|
||||
advancedFields = advancedEnvConfigFields,
|
||||
preservedFields = setOf(
|
||||
ConfigContract::host.name,
|
||||
ConfigContract::apiPrefix.name,
|
||||
ConfigContract::proxyToken.name
|
||||
),
|
||||
snackbarHostState = snackbarHostState,
|
||||
title = stringResource(id = R.string.configuration_title_network_configuration)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ProtonSnackbarHost(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
hostState = snackbarHostState
|
||||
)
|
||||
ProtonTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Column {
|
||||
ConfigurationScreen(
|
||||
snackbarHostState = snackbarHostState,
|
||||
title = stringResource(id = R.string.configuration_title_network_configuration)
|
||||
)
|
||||
}
|
||||
}
|
||||
ProtonSnackbarHost(snackbarHostState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package me.proton.core.configuration.configurator.presentation.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
@ -17,19 +16,19 @@ import androidx.compose.material.MaterialTheme
|
|||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.core.compose.component.ProtonOutlinedTextField
|
||||
import me.proton.core.compose.component.ProtonSnackbarHostState
|
||||
import me.proton.core.compose.component.ProtonSnackbarType
|
||||
|
@ -37,44 +36,186 @@ import me.proton.core.compose.component.ProtonSolidButton
|
|||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.configuration.configurator.R
|
||||
import me.proton.core.configuration.configurator.domain.ConfigurationUseCase
|
||||
import me.proton.core.configuration.configurator.presentation.viewModel.ConfigurationScreenViewModel
|
||||
import me.proton.core.util.kotlin.EMPTY_STRING
|
||||
import me.proton.core.presentation.R.drawable as CoreDrawable
|
||||
|
||||
typealias FieldActionMap = Map<String, (suspend () -> Any)?>
|
||||
@Composable
|
||||
fun ConfigurationScreen(
|
||||
configViewModel: ConfigurationScreenViewModel = hiltViewModel(),
|
||||
snackbarHostState: ProtonSnackbarHostState,
|
||||
title: String,
|
||||
) {
|
||||
val configurationState by configViewModel.state.collectAsStateWithLifecycle()
|
||||
|
||||
ConfigSettingsScreen(
|
||||
configFieldSet = configurationState.configFieldSet,
|
||||
isAdvancedExpanded = configurationState.isAdvanced,
|
||||
title = title,
|
||||
onConfigurationFieldUpdate = { key, newValue ->
|
||||
configViewModel.perform(ConfigurationScreenViewModel.Action.UpdateConfigField(key, newValue))
|
||||
},
|
||||
onAdvanceSetting = {
|
||||
configViewModel.perform(ConfigurationScreenViewModel.Action.SetDefaultConfigFields)
|
||||
},
|
||||
onConfigurationSave = {
|
||||
configViewModel.perform(ConfigurationScreenViewModel.Action.SaveConfig(it))
|
||||
},
|
||||
onAdvancedExpanded = {
|
||||
configViewModel.perform(ConfigurationScreenViewModel.Action.SetAdvanced(it))
|
||||
},
|
||||
onConfigurationFieldFetch = {
|
||||
configViewModel.perform(ConfigurationScreenViewModel.Action.FetchConfigField(it))
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
configViewModel.errorFlow.collect {
|
||||
snackbarHostState.showSnackbar(
|
||||
type = ProtonSnackbarType.ERROR,
|
||||
message = it,
|
||||
actionLabel = "OK"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun <T : Any> ConfigurationScreen(
|
||||
configViewModel: ConfigurationScreenViewModel<T>,
|
||||
advancedFields: FieldActionMap,
|
||||
basicFields: FieldActionMap,
|
||||
preservedFields: Set<String>,
|
||||
snackbarHostState: ProtonSnackbarHostState,
|
||||
title: String
|
||||
private fun ConfigSettingsScreen(
|
||||
configFieldSet: Set<ConfigurationUseCase.ConfigField>,
|
||||
title: String,
|
||||
onConfigurationFieldUpdate: (String, Any) -> Unit,
|
||||
onAdvanceSetting: () -> Unit,
|
||||
onConfigurationFieldFetch: (String) -> Unit,
|
||||
onConfigurationSave: (Boolean) -> Unit,
|
||||
isAdvancedExpanded: Boolean,
|
||||
onAdvancedExpanded: (Boolean) -> Unit
|
||||
) {
|
||||
var isAdvancedExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
ProtonTopAppBar(title = { Text(title) })
|
||||
|
||||
ExpandableHeader(isExpanded = isAdvancedExpanded, onExpandChange = { isAdvancedExpanded = it })
|
||||
ExpandableHeader(isExpanded = isAdvancedExpanded) {
|
||||
onAdvancedExpanded(it)
|
||||
}
|
||||
|
||||
val configFields = if (isAdvancedExpanded) advancedFields else basicFields
|
||||
ConfigurationFields(configViewModel, configFields)
|
||||
ConfigurationFields(
|
||||
configFields = configFieldSet,
|
||||
onFieldUpdate = onConfigurationFieldUpdate,
|
||||
onConfigurationFieldFetch = onConfigurationFieldFetch
|
||||
)
|
||||
|
||||
AdvancedOptionsColumn(isAdvancedExpanded, preservedFields, configViewModel)
|
||||
AdvancedOptionsColumn(
|
||||
isAdvancedExpanded = isAdvancedExpanded,
|
||||
onClick = onAdvanceSetting
|
||||
)
|
||||
|
||||
SaveConfigurationButton(configFields.keys, configViewModel)
|
||||
SaveConfigurationButton(
|
||||
onClick = {
|
||||
onConfigurationSave(isAdvancedExpanded)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ObserveEvents(configViewModel, snackbarHostState)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandableHeader(isExpanded: Boolean, onExpandChange: (Boolean) -> Unit) {
|
||||
private fun ConfigurationFields(
|
||||
configFields: Set<ConfigurationUseCase.ConfigField>,
|
||||
onFieldUpdate: (String, Any) -> Unit,
|
||||
onConfigurationFieldFetch: (String) -> Unit,
|
||||
) {
|
||||
configFields.forEach { configField ->
|
||||
val fetchAction = configField.fetcher?.let { { onConfigurationFieldFetch(configField.name) } }
|
||||
when (configField.value) {
|
||||
is String -> ConfigurationTextField(
|
||||
configField = configField,
|
||||
onValueChange = { newValue ->
|
||||
onFieldUpdate(configField.name, newValue)
|
||||
},
|
||||
fetchAction = fetchAction
|
||||
)
|
||||
|
||||
is Boolean -> ConfigurationCheckbox(
|
||||
configField = configField,
|
||||
onCheckChanged = { newValue ->
|
||||
onFieldUpdate(configField.name, newValue)
|
||||
}
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
else -> error("Unsupported configuration field type for key ${configField.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigurationTextField(
|
||||
configField: ConfigurationUseCase.ConfigField,
|
||||
onValueChange: (String) -> Unit,
|
||||
fetchAction: (() -> Unit)? = null,
|
||||
) {
|
||||
val initialValue = configField.value?.toString() ?: EMPTY_STRING
|
||||
val initialTextFieldValue = remember(initialValue) { TextFieldValue(initialValue) }
|
||||
var textFieldValue by remember { mutableStateOf(initialTextFieldValue) }
|
||||
|
||||
LaunchedEffect(initialValue) {
|
||||
if (textFieldValue.text != initialValue)
|
||||
textFieldValue = TextFieldValue(text = initialValue)
|
||||
}
|
||||
|
||||
ProtonOutlinedTextField(
|
||||
modifier = Modifier.bottomPad(8.dp),
|
||||
value = textFieldValue,
|
||||
onValueChange = { newValue ->
|
||||
textFieldValue = newValue
|
||||
onValueChange(newValue.text)
|
||||
},
|
||||
label = { Text(text = configField.name) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(autoCorrect = false),
|
||||
trailingIcon = {
|
||||
fetchAction?.let {
|
||||
ConfigActionButton(onClick = fetchAction)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigurationCheckbox(
|
||||
configField: ConfigurationUseCase.ConfigField,
|
||||
onCheckChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.bottomPad(8.dp)
|
||||
.clickable {
|
||||
onCheckChanged(
|
||||
!configField.value
|
||||
.toString()
|
||||
.toBoolean()
|
||||
)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = configField.value.toString().toBoolean(),
|
||||
onCheckedChange = onCheckChanged,
|
||||
)
|
||||
Text(text = configField.name.toSpacedWords())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandableHeader(
|
||||
isExpanded: Boolean,
|
||||
onExpandChange: (Boolean) -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = ProtonTheme.colors.floatyText)
|
||||
.clickable { onExpandChange(!isExpanded) }
|
||||
) {
|
||||
Text(
|
||||
|
@ -84,7 +225,7 @@ private fun ExpandableHeader(isExpanded: Boolean, onExpandChange: (Boolean) -> U
|
|||
.background(color = ProtonTheme.colors.floatyText, shape = MaterialTheme.shapes.small)
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = if (isExpanded) R.drawable.ic_proton_arrow_up else R.drawable.ic_proton_arrow_down),
|
||||
painter = painterResource(id = isExpanded.drawable),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.padding(end = 8.dp),
|
||||
|
@ -95,113 +236,38 @@ private fun ExpandableHeader(isExpanded: Boolean, onExpandChange: (Boolean) -> U
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> ConfigurationFields(configViewModel: ConfigurationScreenViewModel<T>, configFields: FieldActionMap) {
|
||||
configFields.forEach { (key, value) ->
|
||||
when (configViewModel.configFieldMap[key]) {
|
||||
is String -> ConfigurationTextField(configViewModel, key, value)
|
||||
is Boolean -> ConfigurationCheckbox(configViewModel, key)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> AdvancedOptionsColumn(
|
||||
private fun AdvancedOptionsColumn(
|
||||
isAdvancedExpanded: Boolean,
|
||||
preservedFields: Set<String>,
|
||||
configViewModel: ConfigurationScreenViewModel<T>,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
if (isAdvancedExpanded) {
|
||||
Column(modifier = Modifier.bottomPad(16.dp), horizontalAlignment = Alignment.End) {
|
||||
ProtonSolidButton(
|
||||
modifier = Modifier.bottomPad(8.dp),
|
||||
onClick = { configViewModel.setDefaultConfigurationFields(preservedFields) }
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(stringResource(id = R.string.configuration_restore_confirmation))
|
||||
Text(stringResource(id = R.string.configuration_set_defaults))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveConfigurationButton(keys: Set<String>, configViewModel: ConfigurationScreenViewModel<*>) {
|
||||
private fun SaveConfigurationButton(onClick: () -> Unit) {
|
||||
Column(modifier = Modifier.bottomPad(16.dp), horizontalAlignment = Alignment.End) {
|
||||
ProtonSolidButton(
|
||||
modifier = Modifier.bottomPad(8.dp),
|
||||
onClick = { configViewModel.saveConfiguration(keys) }
|
||||
onClick = onClick
|
||||
) {
|
||||
Text(stringResource(id = R.string.configuration_button_apply))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> ConfigurationTextField(
|
||||
configViewModel: ConfigurationScreenViewModel<T>,
|
||||
configPropertyKey: String,
|
||||
trailingAction: (suspend () -> Any)? = null
|
||||
) {
|
||||
val fieldValue by configViewModel.observeField(configPropertyKey, EMPTY_STRING).collectAsState()
|
||||
var textState by remember { mutableStateOf(TextFieldValue(fieldValue)) }
|
||||
|
||||
if (fieldValue != textState.text) {
|
||||
textState = TextFieldValue(fieldValue)
|
||||
}
|
||||
|
||||
ProtonOutlinedTextField(
|
||||
modifier = Modifier.bottomPad(8.dp),
|
||||
value = textState,
|
||||
onValueChange = { newValue ->
|
||||
textState = newValue
|
||||
configViewModel.updateConfigField(configPropertyKey, newValue.text)
|
||||
},
|
||||
label = { Text(text = configPropertyKey) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(autoCorrect = false),
|
||||
trailingIcon = {
|
||||
if (trailingAction != null) {
|
||||
ConfigActionButton(onClick = {
|
||||
configViewModel.fetchConfigField(configPropertyKey, trailingAction)
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> ConfigurationCheckbox(
|
||||
configViewModel: ConfigurationScreenViewModel<T>,
|
||||
configPropertyKey: String
|
||||
) {
|
||||
var checkboxState by remember { mutableStateOf(configViewModel.configFieldMap[configPropertyKey] as Boolean) }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.bottomPad(8.dp)
|
||||
.clickable {
|
||||
checkboxState = !checkboxState
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checkboxState,
|
||||
onCheckedChange = { isChecked ->
|
||||
checkboxState = isChecked
|
||||
configViewModel.updateConfigField(configPropertyKey, isChecked)
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = configPropertyKey.toSpacedWords(),
|
||||
modifier = Modifier.bottomPad(8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ConfigActionButton(
|
||||
onClick: () -> Unit,
|
||||
@DrawableRes drawableId: Int = CoreDrawable.ic_proton_arrow_down_circle,
|
||||
onClick: () -> Unit = { },
|
||||
) =
|
||||
IconButton(onClick) {
|
||||
Icon(
|
||||
|
@ -211,31 +277,8 @@ private fun ConfigActionButton(
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T : Any> ObserveEvents(
|
||||
configurationScreenViewModel: ConfigurationScreenViewModel<T>,
|
||||
snackbarHostState: ProtonSnackbarHostState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
configurationScreenViewModel.errorEvent.collect { throwable ->
|
||||
snackbarHostState.showSnackbar(
|
||||
type = ProtonSnackbarType.ERROR,
|
||||
message = throwable.message ?: "Unknown error",
|
||||
actionLabel = "OK"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
configurationScreenViewModel.infoEvent.collect { info ->
|
||||
Toast.makeText(context, info, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.bottomPad(bottomPadding: Dp) = fillMaxWidth().padding(bottom = bottomPadding)
|
||||
|
||||
private fun String.toSpacedWords(): String = replace("(?<=\\p{Lower})(?=[A-Z])".toRegex(), " ").capitalize()
|
||||
private val Boolean.drawable: Int @DrawableRes get() = if (this) R.drawable.ic_proton_arrow_up else R.drawable.ic_proton_arrow_down
|
||||
|
||||
fun String.toSpacedWords(): String = replace("(?<=\\p{Lower})(?=[A-Z])".toRegex(), " ").capitalize()
|
||||
|
|
|
@ -20,98 +20,109 @@ package me.proton.core.configuration.configurator.presentation.viewModel
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
import me.proton.core.compose.viewmodel.stopTimeoutMillis
|
||||
import me.proton.core.configuration.configurator.domain.ConfigurationUseCase
|
||||
import javax.inject.Inject
|
||||
|
||||
typealias ConfigFieldMapper<T> = (Map<String, Any?>) -> T
|
||||
|
||||
class ConfigurationScreenViewModel<T : Any>(
|
||||
private val contentResolverConfigManager: ContentResolverConfigManager,
|
||||
private val configFieldMapper: ConfigFieldMapper<T>,
|
||||
private val defaultConfig: T
|
||||
@HiltViewModel
|
||||
class ConfigurationScreenViewModel @Inject constructor(
|
||||
private val configurationUseCase: ConfigurationUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
private val _configState: MutableStateFlow<T> = MutableStateFlow(defaultConfig)
|
||||
val configurationState: StateFlow<T> get() = _configState
|
||||
|
||||
private val _errorEvent = MutableSharedFlow<Throwable>()
|
||||
val errorEvent: SharedFlow<Throwable> get() = _errorEvent
|
||||
|
||||
private val _infoEvent = MutableSharedFlow<String>()
|
||||
val infoEvent: SharedFlow<String> get() = _infoEvent
|
||||
|
||||
val configFieldMap get() = _configState.value.primitiveFieldMap
|
||||
|
||||
init {
|
||||
fetchInitialConfig()
|
||||
sealed class Action {
|
||||
data object ObserveConfig : Action()
|
||||
data object FetchConfig: Action()
|
||||
data object SetDefaultConfigFields : Action()
|
||||
data class SaveConfig(val isAdvanced: Boolean) : Action()
|
||||
data class SetAdvanced(val isAdvanced: Boolean) : Action()
|
||||
data class FetchConfigField(val key: String) : Action()
|
||||
data class UpdateConfigField(val key: String, val value: Any) : Action()
|
||||
}
|
||||
|
||||
fun fetchConfigField(fieldName: String, configurationFieldGetter: suspend () -> Any) {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
_infoEvent.emit("Fetching $fieldName")
|
||||
configurationFieldGetter()
|
||||
}
|
||||
.onFailure {
|
||||
_errorEvent.emit(it)
|
||||
}
|
||||
.onSuccess { newValue ->
|
||||
updateConfigField(fieldName, newValue)
|
||||
}
|
||||
data class State(
|
||||
val configFieldSet: Set<ConfigurationUseCase.ConfigField>,
|
||||
val isAdvanced: Boolean
|
||||
)
|
||||
|
||||
private val mutableErrorFlow: MutableSharedFlow<String> = MutableSharedFlow()
|
||||
|
||||
private val isAdvanced: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
|
||||
val errorFlow: SharedFlow<String> = mutableErrorFlow.asSharedFlow()
|
||||
|
||||
val state: StateFlow<State> =
|
||||
observeConfig().onStart {
|
||||
perform(Action.FetchConfig)
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(stopTimeoutMillis),
|
||||
initialValue = State(configurationUseCase.configState.value, isAdvanced.value)
|
||||
)
|
||||
|
||||
fun perform(action: Action) = runCatching {
|
||||
when (action) {
|
||||
is Action.SetDefaultConfigFields -> setDefaultConfigFields()
|
||||
is Action.ObserveConfig -> observeConfig()
|
||||
is Action.FetchConfig -> fetchConfig()
|
||||
is Action.SaveConfig -> saveConfig(action.isAdvanced)
|
||||
is Action.SetAdvanced -> setAdvanced(action.isAdvanced)
|
||||
is Action.FetchConfigField -> fetchConfigField(action.key)
|
||||
is Action.UpdateConfigField -> updateConfigField(action.key, action.value)
|
||||
}
|
||||
}.onFailure {
|
||||
mutableErrorFlow.tryEmit(it.message ?: "Unknown message")
|
||||
}
|
||||
|
||||
fun saveConfiguration(keysToSave: Set<String> = _configState.value.primitiveFieldMap.keys) {
|
||||
private fun observeConfig(): Flow<State> = combine(
|
||||
configurationUseCase.configState,
|
||||
isAdvanced
|
||||
) { fieldSet, advanced ->
|
||||
val fieldList = if (isAdvanced.value) fieldSet else fieldSet.filter { it.isAdvanced == advanced }
|
||||
State(fieldList.toSet(), isAdvanced.value)
|
||||
}
|
||||
|
||||
private fun setDefaultConfigFields() = launchCatching {
|
||||
configurationUseCase.setDefaultConfigurationFields()
|
||||
}
|
||||
|
||||
private fun fetchConfig() = launchCatching {
|
||||
configurationUseCase.fetchConfig()
|
||||
}
|
||||
|
||||
private fun saveConfig(isAdvanced: Boolean) = launchCatching {
|
||||
configurationUseCase.saveConfig(isAdvanced)
|
||||
}
|
||||
|
||||
private fun fetchConfigField(key: String) = launchCatching {
|
||||
configurationUseCase.fetchConfigField(key)
|
||||
}
|
||||
|
||||
private fun updateConfigField(key: String, value: Any) = launchCatching {
|
||||
configurationUseCase.updateConfigField(key, value)
|
||||
}
|
||||
|
||||
private fun setAdvanced(advancedValue: Boolean) = launchCatching {
|
||||
isAdvanced.emit(advancedValue)
|
||||
}
|
||||
|
||||
private fun launchCatching(block: suspend () -> Unit) =
|
||||
viewModelScope.launch {
|
||||
val mapToInsert = keysToSave.associateWith { _configState.value.primitiveFieldMap[it] }
|
||||
runCatching {
|
||||
contentResolverConfigManager.insertContentValuesAtPath(
|
||||
mapToInsert,
|
||||
_configState.value::class.java.name
|
||||
)
|
||||
}.onFailure { _errorEvent.emit(it) }.onSuccess {
|
||||
_infoEvent.emit("Configuration Saved")
|
||||
block()
|
||||
}.onFailure {
|
||||
mutableErrorFlow.emit(it.message ?: "Unknown error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultConfigurationFields(preservedFields: Set<String> = configFieldMap.keys) {
|
||||
val map = preservedFields.associateWith { configFieldMap[it].toString() }
|
||||
_configState.value = configFieldMapper(map)
|
||||
}
|
||||
|
||||
fun <R> observeField(key: String, defaultValue: R): StateFlow<R> =
|
||||
_configState.map { state ->
|
||||
state.primitiveFieldMap[key] as? R ?: defaultValue
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), defaultValue)
|
||||
|
||||
private fun fetchInitialConfig() {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
val configMap =
|
||||
contentResolverConfigManager.fetchConfigurationDataAtPath(defaultConfig::class.java.name)
|
||||
configFieldMapper(configMap ?: emptyMap())
|
||||
}.onFailure { _errorEvent.emit(it) }
|
||||
.onSuccess { config ->
|
||||
_configState.value = config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConfigField(updatedField: String, newValue: Any) {
|
||||
_configState.value = _configState.value.withUpdatedField(updatedField, newValue).also {
|
||||
println("Updating field: $updatedField, $newValue")
|
||||
}
|
||||
}
|
||||
|
||||
private fun T.withUpdatedField(updatedField: String, newValue: Any): T =
|
||||
configFieldMapper(this.primitiveFieldMap.toMutableMap().apply { this[updatedField] = newValue })
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<resources>
|
||||
<string name="app_name" translatable="false">Configurator</string>
|
||||
<string name="configuration_button_apply" translatable="false">Apply</string>
|
||||
<string name="configuration_restore_confirmation" translatable="false">Set</string>
|
||||
<string name="configuration_set_defaults" translatable="false">Set</string>
|
||||
<string name="configuration_title_network_configuration" translatable="false">Network Configuration</string>
|
||||
<string name="configuration_text_advanced" translatable="false">Advanced</string>
|
||||
<string name="configuration_error_unknown" translatable="false">Could not save configuration. Unknown error</string>
|
||||
|
|
|
@ -37,9 +37,7 @@ public class ContentResolverEnvironmentConfigModule {
|
|||
contentResolverConfigManager: ContentResolverConfigManager
|
||||
): EnvironmentConfiguration {
|
||||
val staticEnvironmentConfig = EnvironmentConfiguration.fromClass()
|
||||
val contentResolverConfigData = contentResolverConfigManager.fetchConfigurationDataAtPath(
|
||||
EnvironmentConfiguration::class.java.name
|
||||
)
|
||||
val contentResolverConfigData = contentResolverConfigManager.queryAtClassPath(EnvironmentConfiguration::class)
|
||||
return EnvironmentConfiguration.fromMap(contentResolverConfigData ?: return staticEnvironmentConfig)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
public class me/proton/core/configuration/ContentResolverConfigManager {
|
||||
public static final field Companion Lme/proton/core/configuration/ContentResolverConfigManager$Companion;
|
||||
public fun <init> (Landroid/content/Context;)V
|
||||
public final fun fetchConfigurationDataAtPath (Ljava/lang/String;)Ljava/util/Map;
|
||||
public final fun getContext ()Landroid/content/Context;
|
||||
public final fun insertContentValuesAtPath (Ljava/util/Map;Ljava/lang/String;)Landroid/net/Uri;
|
||||
public final fun insertConfigFieldMapAtClassPath (Ljava/util/Map;Lkotlin/reflect/KClass;)Landroid/net/Uri;
|
||||
public final fun queryAtClassPath (Lkotlin/reflect/KClass;)Ljava/util/Map;
|
||||
}
|
||||
|
||||
public final class me/proton/core/configuration/ContentResolverConfigManager$Companion {
|
||||
|
@ -11,25 +11,26 @@ public final class me/proton/core/configuration/ContentResolverConfigManager$Com
|
|||
|
||||
public final class me/proton/core/configuration/EnvironmentConfiguration : me/proton/core/configuration/entity/ConfigContract {
|
||||
public static final field Companion Lme/proton/core/configuration/EnvironmentConfiguration$Companion;
|
||||
public fun <init> (Lkotlin/reflect/KFunction;)V
|
||||
public final fun component1 ()Lkotlin/reflect/KFunction;
|
||||
public final fun copy (Lkotlin/reflect/KFunction;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public static synthetic fun copy$default (Lme/proton/core/configuration/EnvironmentConfiguration;Lkotlin/reflect/KFunction;ILjava/lang/Object;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public fun <init> (Lme/proton/core/configuration/entity/EnvironmentConfigFieldProvider;)V
|
||||
public final fun component1 ()Lme/proton/core/configuration/entity/EnvironmentConfigFieldProvider;
|
||||
public final fun copy (Lme/proton/core/configuration/entity/EnvironmentConfigFieldProvider;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public static synthetic fun copy$default (Lme/proton/core/configuration/EnvironmentConfiguration;Lme/proton/core/configuration/entity/EnvironmentConfigFieldProvider;ILjava/lang/Object;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public fun getApiHost ()Ljava/lang/String;
|
||||
public fun getApiPrefix ()Ljava/lang/String;
|
||||
public fun getBaseUrl ()Ljava/lang/String;
|
||||
public final fun getConfigFieldProvider ()Lme/proton/core/configuration/entity/EnvironmentConfigFieldProvider;
|
||||
public fun getHost ()Ljava/lang/String;
|
||||
public fun getHv3Host ()Ljava/lang/String;
|
||||
public fun getHv3Url ()Ljava/lang/String;
|
||||
public fun getProxyToken ()Ljava/lang/String;
|
||||
public final fun getStringProvider ()Lkotlin/reflect/KFunction;
|
||||
public fun getUseDefaultPins ()Z
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/configuration/EnvironmentConfiguration$Companion {
|
||||
public final fun fromBundle (Landroid/os/Bundle;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public final fun fromClass (Ljava/lang/String;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public static synthetic fun fromClass$default (Lme/proton/core/configuration/EnvironmentConfiguration$Companion;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public final fun fromMap (Ljava/util/Map;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
|
@ -53,8 +54,35 @@ public abstract interface class me/proton/core/configuration/entity/ConfigContra
|
|||
public abstract fun getUseDefaultPins ()Z
|
||||
}
|
||||
|
||||
public abstract interface class me/proton/core/configuration/entity/EnvironmentConfigFieldProvider {
|
||||
public abstract fun getBoolean (Ljava/lang/String;)Ljava/lang/Boolean;
|
||||
public abstract fun getInt (Ljava/lang/String;)Ljava/lang/Integer;
|
||||
public abstract fun getString (Ljava/lang/String;)Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/configuration/extension/EnvironmentConfigurationKt {
|
||||
public static final fun getConfigContractFields (Ljava/lang/Object;)Ljava/util/Map;
|
||||
public static final fun getPrimitiveFieldMap (Ljava/lang/Object;)Ljava/util/Map;
|
||||
}
|
||||
|
||||
public final class me/proton/core/configuration/provider/BundleConfigFieldProvider : me/proton/core/configuration/entity/EnvironmentConfigFieldProvider {
|
||||
public fun <init> (Landroid/os/Bundle;)V
|
||||
public fun getBoolean (Ljava/lang/String;)Ljava/lang/Boolean;
|
||||
public fun getInt (Ljava/lang/String;)Ljava/lang/Integer;
|
||||
public fun getString (Ljava/lang/String;)Ljava/lang/String;
|
||||
}
|
||||
|
||||
public class me/proton/core/configuration/provider/MapConfigFieldProvider : me/proton/core/configuration/entity/EnvironmentConfigFieldProvider {
|
||||
public fun <init> (Ljava/util/Map;)V
|
||||
public fun getBoolean (Ljava/lang/String;)Ljava/lang/Boolean;
|
||||
public fun getInt (Ljava/lang/String;)Ljava/lang/Integer;
|
||||
public final fun getMap ()Ljava/util/Map;
|
||||
public fun getString (Ljava/lang/String;)Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/configuration/provider/StaticClassConfigFieldProvider : me/proton/core/configuration/entity/EnvironmentConfigFieldProvider {
|
||||
public fun <init> (Ljava/lang/String;)V
|
||||
public fun getBoolean (Ljava/lang/String;)Ljava/lang/Boolean;
|
||||
public fun getInt (Ljava/lang/String;)Ljava/lang/Integer;
|
||||
public fun getString (Ljava/lang/String;)Ljava/lang/String;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
|
||||
import studio.forface.easygradle.dsl.*
|
||||
import studio.forface.easygradle.dsl.android.*
|
||||
|
||||
plugins {
|
||||
protonAndroidLibrary
|
||||
|
@ -30,12 +31,22 @@ android {
|
|||
}
|
||||
|
||||
protonCoverage {
|
||||
branchCoveragePercentage.set(79)
|
||||
lineCoveragePercentage.set(85)
|
||||
branchCoveragePercentage.set(62)
|
||||
lineCoveragePercentage.set(77)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(Module.networkData))
|
||||
|
||||
testImplementation(junit, mockk)
|
||||
testImplementation(
|
||||
junit,
|
||||
mockk
|
||||
)
|
||||
|
||||
androidTestImplementation(
|
||||
junit,
|
||||
`android-test-core-ktx`,
|
||||
`android-test-runner`,
|
||||
`android-test-rules`
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!--
|
||||
~ Copyright (c) 2022 Proton Technologies AG
|
||||
~ This file is part of Proton AG and ProtonCore.
|
||||
~
|
||||
~ ProtonCore is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ ProtonCore is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="me.proton.core.configuration.ACCESS_DATA"/>
|
||||
</manifest>
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import android.os.Bundle
|
||||
import me.proton.core.configuration.provider.BundleConfigFieldProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class BundleConfigFieldProviderTest {
|
||||
|
||||
@Test
|
||||
fun getString_returnsCorrectValue() {
|
||||
val bundle = Bundle().apply {
|
||||
putString("testStringKey", "testStringValue")
|
||||
}
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertEquals("testStringValue", provider.getString("testStringKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getString_returnsNullForNonexistentKey() {
|
||||
val bundle = Bundle()
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertNull(provider.getString("nonexistentKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoolean_returnsCorrectValue() {
|
||||
val bundle = Bundle().apply {
|
||||
putBoolean("testBooleanKey", true)
|
||||
}
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertTrue(provider.getBoolean("testBooleanKey")!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoolean_returnsNullForNonexistentKey() {
|
||||
val bundle = Bundle()
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertNull(provider.getBoolean("nonexistentKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInt_returnsCorrectValue() {
|
||||
val bundle = Bundle().apply {
|
||||
putInt("testIntKey", 123)
|
||||
}
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertEquals(123, provider.getInt("testIntKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getInt_returnsNullForNonexistentKey() {
|
||||
val bundle = Bundle()
|
||||
val provider = BundleConfigFieldProvider(bundle)
|
||||
assertNull(provider.getInt("nonexistentKey"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import junit.framework.TestCase.assertNotNull
|
||||
import junit.framework.TestCase.assertNull
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import me.proton.core.configuration.entity.ConfigContract
|
||||
import org.junit.Test
|
||||
|
||||
class ContentResolverConfigManagerTest {
|
||||
@Test
|
||||
fun queryAtClassPath_ReturnsNonNullMap() {
|
||||
val appContext = ApplicationProvider.getApplicationContext<Context>()
|
||||
val manager = ContentResolverConfigManager(appContext)
|
||||
|
||||
val result = manager.queryAtClassPath(MyConfigClass::class)
|
||||
|
||||
assertNotNull(result)
|
||||
assertTrue(result!!.isNotEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertConfigFieldMapAtClassPath_InsertsDataCorrectly() {
|
||||
val appContext = ApplicationProvider.getApplicationContext<Context>()
|
||||
val manager = ContentResolverConfigManager(appContext)
|
||||
|
||||
val testMap = mapOf("testKey" to "testValue")
|
||||
val insertUri = manager.insertConfigFieldMapAtClassPath(testMap, MyConfigClass::class)
|
||||
|
||||
assertNotNull(insertUri)
|
||||
|
||||
val queryResult = manager.queryAtClassPath(MyConfigClass::class)
|
||||
assertTrue("testValue" == queryResult?.get("testKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun queryAtClassPathWithInternalClassReturnsNull() {
|
||||
class Internal {
|
||||
val host = "test"
|
||||
}
|
||||
|
||||
val appContext = ApplicationProvider.getApplicationContext<Context>()
|
||||
val manager = ContentResolverConfigManager(appContext)
|
||||
|
||||
val result = manager.queryAtClassPath(Internal::class)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
}
|
||||
|
||||
class MyConfigClass: ConfigContract {
|
||||
override val host: String
|
||||
get() = "host"
|
||||
override val proxyToken: String
|
||||
get() = "proxyToken"
|
||||
override val apiPrefix: String
|
||||
get() = "apiPrefix"
|
||||
override val apiHost: String
|
||||
get() = "apiHost"
|
||||
override val baseUrl: String
|
||||
get() = "baseUrl"
|
||||
override val hv3Host: String
|
||||
get() = "hv3Host"
|
||||
override val hv3Url: String
|
||||
get() = "hv3Url"
|
||||
override val useDefaultPins: Boolean
|
||||
get() = false
|
||||
}
|
|
@ -22,30 +22,45 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
public open class ContentResolverConfigManager(
|
||||
public val context: Context
|
||||
) {
|
||||
private val String.contentResolverUrl: Uri get() = Uri.parse("content://$CONFIG_AUTHORITY/config/$this")
|
||||
|
||||
@Synchronized
|
||||
public fun fetchConfigurationDataAtPath(path: String): Map<String, Any?>? = context.contentResolver.query(
|
||||
path.contentResolverUrl,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)?.use { cursor ->
|
||||
cursor.columnNames.associateWith { columnName ->
|
||||
cursor.retrieveValue(columnName)
|
||||
public fun queryAtClassPath(clazz: KClass<*>): Map<String, Any?>? {
|
||||
val cursor = context.contentResolver.query(
|
||||
clazz.qualifiedName?.contentResolverUrl ?: return null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
return cursor?.use {
|
||||
cursor.columnNames.associateWith { columnName ->
|
||||
cursor.retrieveValue(columnName)
|
||||
}
|
||||
}?.takeIf {
|
||||
it.isNotEmpty()
|
||||
}
|
||||
}?.takeIf {
|
||||
it.isNotEmpty()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
public fun insertContentValuesAtPath(configFieldMap: Map<String, Any?>, path: String): Uri? =
|
||||
context.contentResolver.insert(path.contentResolverUrl, contentValues(configFieldMap))
|
||||
public fun insertConfigFieldMapAtClassPath(configFieldMap: Map<String, Any?>, clazz: KClass<*>): Uri? =
|
||||
context.contentResolver.insert(clazz.qualifiedName!!.contentResolverUrl, configFieldMap.contentValues)
|
||||
|
||||
private val String.contentResolverUrl: Uri get() = Uri.parse("content://$CONFIG_AUTHORITY/config/$this")
|
||||
private val Map<String, Any?>.contentValues: ContentValues get() = ContentValues().apply {
|
||||
forEach { (key, value) ->
|
||||
when (value) {
|
||||
is String -> put(key, value)
|
||||
is Boolean -> put(key, value)
|
||||
is Int -> put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Cursor.retrieveValue(columnName: String): Any? {
|
||||
val columnIndex = getColumnIndex(columnName)
|
||||
|
@ -53,15 +68,6 @@ public open class ContentResolverConfigManager(
|
|||
return if (moveToFirst()) getString(columnIndex) else null
|
||||
}
|
||||
|
||||
private fun contentValues(map: Map<String, Any?>): ContentValues = ContentValues().apply {
|
||||
map.forEach { (key, value) ->
|
||||
when (value) {
|
||||
is String -> put(key, value)
|
||||
is Boolean -> put(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public companion object {
|
||||
private const val CONFIG_AUTHORITY = "me.proton.core.configuration.configurator"
|
||||
}
|
||||
|
|
|
@ -18,49 +18,35 @@
|
|||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import android.os.Bundle
|
||||
import me.proton.core.configuration.entity.ConfigContract
|
||||
import kotlin.reflect.KFunction1
|
||||
import kotlin.reflect.KProperty
|
||||
import me.proton.core.configuration.entity.EnvironmentConfigFieldProvider
|
||||
import me.proton.core.configuration.provider.BundleConfigFieldProvider
|
||||
import me.proton.core.configuration.provider.MapConfigFieldProvider
|
||||
import me.proton.core.configuration.provider.StaticClassConfigFieldProvider
|
||||
|
||||
private const val DEFAULT_CONFIG_CLASS: String = "me.proton.core.configuration.EnvironmentConfigurationDefaults"
|
||||
|
||||
public data class EnvironmentConfiguration(
|
||||
val stringProvider: KFunction1<String, Any?>
|
||||
val configFieldProvider: EnvironmentConfigFieldProvider
|
||||
) : ConfigContract {
|
||||
override val host: String = getString(::host) ?: ""
|
||||
override val proxyToken: String = getString(::proxyToken) ?: ""
|
||||
override val apiPrefix: String = getString(::apiPrefix) ?: "api"
|
||||
override val apiHost: String = getString(::apiHost) ?: "$apiPrefix.$host"
|
||||
override val baseUrl: String = getString(::baseUrl) ?: "https://$apiHost"
|
||||
override val hv3Host: String = getString(::hv3Host) ?: "verify.$host"
|
||||
override val hv3Url: String = getString(::hv3Url) ?: "https://$hv3Host"
|
||||
override val useDefaultPins: Boolean = getString(::useDefaultPins) ?: (host == "proton.me")
|
||||
|
||||
private fun <T> getString(propertyName: KProperty<Any>): T = stringProvider(propertyName.name) as T
|
||||
override val host: String = configFieldProvider.getString(::host.name) ?: ""
|
||||
override val proxyToken: String = configFieldProvider.getString(::proxyToken.name) ?: ""
|
||||
override val apiPrefix: String = configFieldProvider.getString(::apiPrefix.name) ?: "api"
|
||||
override val apiHost: String = configFieldProvider.getString(::apiHost.name) ?: "$apiPrefix.$host"
|
||||
override val baseUrl: String = configFieldProvider.getString(::baseUrl.name) ?: "https://$apiHost"
|
||||
override val hv3Host: String = configFieldProvider.getString(::hv3Host.name) ?: "verify.$host"
|
||||
override val hv3Url: String = configFieldProvider.getString(::hv3Url.name) ?: "https://$hv3Host"
|
||||
override val useDefaultPins: Boolean = configFieldProvider.getBoolean(::useDefaultPins.name) ?: (host == "proton.me")
|
||||
|
||||
public companion object {
|
||||
|
||||
public fun fromMap(configMap: Map<String, Any?>): EnvironmentConfiguration =
|
||||
EnvironmentConfiguration(configMap::get)
|
||||
EnvironmentConfiguration(MapConfigFieldProvider(configMap))
|
||||
|
||||
public fun fromClass(className: String = DEFAULT_CONFIG_CLASS): EnvironmentConfiguration =
|
||||
fromMap(getConfigDataMapFromClass(className))
|
||||
EnvironmentConfiguration(StaticClassConfigFieldProvider(className))
|
||||
|
||||
private fun getConfigDataMapFromClass(className: String) = try {
|
||||
val defaultsClass = Class.forName(className)
|
||||
val instance = defaultsClass.newInstance()
|
||||
|
||||
defaultsClass
|
||||
.declaredFields
|
||||
.associate { property ->
|
||||
property.isAccessible = true
|
||||
property.name to property.get(instance)
|
||||
}
|
||||
} catch (e: ClassNotFoundException) {
|
||||
throw IllegalStateException(
|
||||
"Class not found: $className. Make sure environment configuration gradle plugin is enabled!",
|
||||
e
|
||||
)
|
||||
}
|
||||
public fun fromBundle(bundle: Bundle): EnvironmentConfiguration =
|
||||
EnvironmentConfiguration(BundleConfigFieldProvider(bundle))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.entity
|
||||
|
||||
public interface EnvironmentConfigFieldProvider {
|
||||
public fun getString(key: String): String?
|
||||
public fun getBoolean(key: String): Boolean?
|
||||
public fun getInt(key: String): Int?
|
||||
}
|
|
@ -18,17 +18,13 @@
|
|||
|
||||
package me.proton.core.configuration.extension
|
||||
|
||||
import java.lang.reflect.Field
|
||||
|
||||
public val Any.configContractFields: Map<String, Field>
|
||||
get() = this::class.java.declaredFields.associateBy {
|
||||
it.isAccessible = true
|
||||
it.name
|
||||
}
|
||||
|
||||
public val Any.primitiveFieldMap: Map<String, Any?>
|
||||
get() = configContractFields.filter { map ->
|
||||
map.value.get(this).let { it is String || it is Boolean }
|
||||
}.mapValues {
|
||||
it.value.get(this)
|
||||
}
|
||||
get() = this::class.java.declaredFields
|
||||
.associateBy {
|
||||
it.isAccessible = true
|
||||
it.name
|
||||
}
|
||||
.filter { it.value.get(this).isPrimitive() }
|
||||
.mapValues { it.value.get(this) }
|
||||
|
||||
private fun Any?.isPrimitive(): Boolean = this is String || this is Boolean || this is Int
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.provider
|
||||
|
||||
import android.os.Bundle
|
||||
import me.proton.core.configuration.entity.EnvironmentConfigFieldProvider
|
||||
|
||||
public class BundleConfigFieldProvider(
|
||||
private val bundle: Bundle
|
||||
) : EnvironmentConfigFieldProvider {
|
||||
override fun getString(key: String): String? = bundle.getString(key)
|
||||
|
||||
override fun getBoolean(key: String): Boolean? = bundle.takeIf { bundle.containsKey(key) }?.getBoolean(key)
|
||||
|
||||
override fun getInt(key: String): Int? = bundle.takeIf { bundle.containsKey(key) }?.getInt(key)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.provider
|
||||
|
||||
import me.proton.core.configuration.entity.EnvironmentConfigFieldProvider
|
||||
|
||||
public open class MapConfigFieldProvider(
|
||||
public val map: Map<String, Any?>
|
||||
) : EnvironmentConfigFieldProvider {
|
||||
override fun getString(key: String): String? = map[key]?.toString()
|
||||
override fun getInt(key: String): Int? = map[key]?.toString()?.toInt()
|
||||
override fun getBoolean(key: String): Boolean? =
|
||||
map[key].takeIf { map.containsKey(key) && it is Boolean } as? Boolean
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration.provider
|
||||
|
||||
import me.proton.core.configuration.entity.EnvironmentConfigFieldProvider
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
|
||||
public class StaticClassConfigFieldProvider(
|
||||
private val className: String
|
||||
) : EnvironmentConfigFieldProvider {
|
||||
|
||||
private val staticConfigDataMap =
|
||||
runCatching {
|
||||
val defaultsClass = Class.forName(className)
|
||||
val instance = defaultsClass.newInstance()
|
||||
instance.primitiveFieldMap
|
||||
}.onFailure {
|
||||
error("Class not found: $className!")
|
||||
}.getOrThrow()
|
||||
|
||||
private val mapConfigFieldProvider = MapConfigFieldProvider(staticConfigDataMap)
|
||||
|
||||
override fun getString(key: String): String? = mapConfigFieldProvider.getString(key)
|
||||
override fun getBoolean(key: String): Boolean? = mapConfigFieldProvider.getBoolean(key)
|
||||
override fun getInt(key: String): Int? = mapConfigFieldProvider.getInt(key)
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
|
||||
class ConfigFieldTest {
|
||||
|
||||
@Test
|
||||
fun `returns correct String value when present in map`() {
|
||||
val key = "testKey"
|
||||
val expectedValue = "testValue"
|
||||
val configMap: Map<String, Any?> = mapOf(key to expectedValue)
|
||||
|
||||
val actualValue: String = configMap[key] as String
|
||||
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns correct Boolean value when present in map`() {
|
||||
val key = "testKey"
|
||||
val expectedValue = true
|
||||
val configMap: Map<String, Any?> = mapOf(key to expectedValue)
|
||||
|
||||
val actualValue: Boolean = configMap[key] as Boolean
|
||||
|
||||
assertEquals(expectedValue, actualValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns null when value in map is null`() {
|
||||
val key = "testKey"
|
||||
val configMap: Map<String, Any?> = mapOf(key to null)
|
||||
|
||||
val actualValue: String? = configMap[key] as String?
|
||||
|
||||
assertNull(actualValue)
|
||||
}
|
||||
}
|
|
@ -61,7 +61,7 @@ class ContentResolverConfigManagerTest {
|
|||
every { cursor.getString(1) } returns "value2"
|
||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns cursor
|
||||
|
||||
val result = configManager.fetchConfigurationDataAtPath(EnvironmentConfiguration::class.java.name)
|
||||
val result = configManager.queryAtClassPath(EnvironmentConfiguration::class)
|
||||
|
||||
assertEquals(mapOf("key1" to "value1", "key2" to "value2"), result)
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class ContentResolverConfigManagerTest {
|
|||
every { cursor.moveToFirst() } returns false // No data to move to
|
||||
every { contentResolver.query(any(), null, null, null, null) } returns cursor
|
||||
|
||||
val result = configManager.fetchConfigurationDataAtPath("emptyPath")
|
||||
val result = configManager.queryAtClassPath(EnvironmentConfiguration::class)
|
||||
|
||||
assertTrue(result.isNullOrEmpty())
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ class ContentResolverConfigManagerTest {
|
|||
fun `fetchConfigurationDataAtPath returns null for invalid path`() {
|
||||
every { contentResolver.query(any(), null, null, null, null) } returns null
|
||||
|
||||
val result = configManager.fetchConfigurationDataAtPath("invalidPath")
|
||||
val result = configManager.queryAtClassPath(this::class)
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
|
|
@ -18,86 +18,45 @@
|
|||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import me.proton.core.configuration.extension.configContractFields
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.KFunction1
|
||||
|
||||
class EnvironmentConfigurationTest {
|
||||
|
||||
private val mockConfigData: Map<String, Any?> = mapOf(
|
||||
"host" to "testHost",
|
||||
"proxyToken" to "testProxyToken",
|
||||
"apiPrefix" to "testApiPrefix",
|
||||
"baseUrl" to "testBaseUrl",
|
||||
"apiHost" to "api.host",
|
||||
"hv3Host" to "testHv3Host",
|
||||
"hv3Url" to "testHv3Url"
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private val expected = EnvironmentConfiguration(mockConfigData::get as KFunction1<String, String?>)
|
||||
|
||||
private class ValidStaticConfig {
|
||||
val host: String = "testHost"
|
||||
val proxyToken: String = "testProxyToken"
|
||||
val apiPrefix: String = "testApiPrefix"
|
||||
val baseUrl: String = "testBaseUrl"
|
||||
val apiHost: String = "api.host"
|
||||
val hv3Host: String = "testHv3Host"
|
||||
val hv3Url: String = "testHv3Url"
|
||||
}
|
||||
|
||||
private class InvalidStaticConfig {
|
||||
val host = 0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load config from map`() {
|
||||
val actual = EnvironmentConfiguration.fromMap(mockConfigData)
|
||||
assertEquals(
|
||||
actual.configContractFields,
|
||||
expected.configContractFields
|
||||
fun `EnvironmentConfiguration initializes correctly with MapFieldProvider`() {
|
||||
val configMap = mapOf(
|
||||
"host" to "test.proton.me",
|
||||
"proxyToken" to "token123",
|
||||
"apiPrefix" to "apiTest",
|
||||
"useDefaultPins" to false
|
||||
)
|
||||
val config = EnvironmentConfiguration.fromMap(configMap)
|
||||
|
||||
assertEquals("test.proton.me", config.host)
|
||||
assertEquals("token123", config.proxyToken)
|
||||
assertEquals("apiTest", config.apiPrefix)
|
||||
assertEquals("apiTest.test.proton.me", config.apiHost)
|
||||
assertEquals("https://apiTest.test.proton.me", config.baseUrl)
|
||||
assertEquals("verify.test.proton.me", config.hv3Host)
|
||||
assertEquals("https://verify.test.proton.me", config.hv3Url)
|
||||
assertEquals(false, config.useDefaultPins)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throw error for unsupported type when loading from map`() {
|
||||
assertThrows(ClassCastException::class.java) {
|
||||
EnvironmentConfiguration.fromMap(mapOf("host" to arrayOf("")))
|
||||
}
|
||||
}
|
||||
fun `EnvironmentConfiguration uses defaults correctly`() {
|
||||
val minimalConfigMap = mapOf(
|
||||
"host" to "proton.me"
|
||||
)
|
||||
val config = EnvironmentConfiguration.fromMap(minimalConfigMap)
|
||||
|
||||
@Test
|
||||
fun `throw error for loading non-existent class`() {
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
EnvironmentConfiguration.fromClass("null")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throw error for loading invalid config`() {
|
||||
assertThrows(ClassCastException::class.java) {
|
||||
EnvironmentConfiguration.fromClass(InvalidStaticConfig::class.java.name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load config from class`() {
|
||||
val actual = EnvironmentConfiguration.fromClass(ValidStaticConfig::class.java.name)
|
||||
assertEquals(actual.configContractFields, expected.configContractFields)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default proxy usage is set`() {
|
||||
val actual = EnvironmentConfiguration.fromMap(mapOf("host" to "proton.me"))
|
||||
assertEquals(actual.useDefaultPins, true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `default proxy usage is overridden`() {
|
||||
val actual = EnvironmentConfiguration.fromMap(mapOf("host" to "proton.me", "useDefaultPins" to false))
|
||||
assertEquals(actual.useDefaultPins, false)
|
||||
assertEquals("proton.me", config.host)
|
||||
assertEquals("", config.proxyToken) // Default empty string
|
||||
assertEquals("api", config.apiPrefix) // Specified default
|
||||
assertEquals("api.proton.me", config.apiHost) // Constructed from defaults
|
||||
assertEquals("https://api.proton.me", config.baseUrl) // Constructed URL
|
||||
assertEquals("verify.proton.me", config.hv3Host) // Constructed host
|
||||
assertEquals("https://verify.proton.me", config.hv3Url) // Constructed URL
|
||||
assertEquals(true, config.useDefaultPins) // Default based on `host == "proton.me"`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,48 +18,35 @@
|
|||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import me.proton.core.configuration.extension.configContractFields
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class ExtensionFunctionTests {
|
||||
|
||||
class TestClass {
|
||||
private val stringField: String = "TestString"
|
||||
private val booleanField: Boolean = true
|
||||
private val intField: Int = 42
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `configContractFields includes all declared fields`() {
|
||||
val testObject = TestClass()
|
||||
val fields = testObject.configContractFields
|
||||
|
||||
assertEquals(3, fields.size)
|
||||
assertTrue(fields.containsKey("stringField"))
|
||||
assertTrue(fields.containsKey("booleanField"))
|
||||
assertTrue(fields.containsKey("intField"))
|
||||
class MockClass {
|
||||
val stringField = "Test String"
|
||||
val booleanField = true
|
||||
val intField = 123
|
||||
val listField = listOf("Not", "Primitive")
|
||||
val doubleField = 123.45
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `primitiveFieldMap includes only primitive fields`() {
|
||||
val testObject = TestClass()
|
||||
val primitiveFields = testObject.primitiveFieldMap
|
||||
val mockInstance = MockClass()
|
||||
val primitiveFieldMap = mockInstance.primitiveFieldMap
|
||||
|
||||
assertEquals(2, primitiveFields.size)
|
||||
assertEquals("TestString", primitiveFields["stringField"])
|
||||
assertEquals(true, primitiveFields["booleanField"])
|
||||
assertFalse(primitiveFields.containsKey("intField"))
|
||||
}
|
||||
assertEquals(3, primitiveFieldMap.size)
|
||||
assertTrue(primitiveFieldMap.containsKey("stringField"))
|
||||
assertTrue(primitiveFieldMap.containsKey("booleanField"))
|
||||
assertTrue(primitiveFieldMap.containsKey("intField"))
|
||||
|
||||
@Test
|
||||
fun `configContractFields makes fields accessible`() {
|
||||
val testObject = TestClass()
|
||||
val fields = testObject.configContractFields
|
||||
assertEquals("Test String", primitiveFieldMap["stringField"])
|
||||
assertEquals(true, primitiveFieldMap["booleanField"])
|
||||
assertEquals(123, primitiveFieldMap["intField"])
|
||||
|
||||
assertTrue(fields.all { it.value.isAccessible })
|
||||
assertTrue(!primitiveFieldMap.containsKey("listField"))
|
||||
assertTrue(!primitiveFieldMap.containsKey("doubleField"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import me.proton.core.configuration.provider.MapConfigFieldProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class MapConfigFieldProviderTest {
|
||||
|
||||
@Test
|
||||
fun `test getString returns correct String value`() {
|
||||
val map = mapOf("key1" to "value1")
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertEquals("value1", provider.getString("key1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getString returns null for non-existent key`() {
|
||||
val map = mapOf<String, Any?>()
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertNull(provider.getString("key2"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getBoolean returns true for true String value`() {
|
||||
val map = mapOf("key" to true)
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertTrue(provider.getBoolean("key")!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getBoolean returns false for false String value`() {
|
||||
val map = mapOf("key" to false)
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertFalse(provider.getBoolean("key")!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getBoolean returns null for non-boolean String value`() {
|
||||
val map = mapOf("key" to "notABoolean")
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertNull(provider.getBoolean("key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getInt returns correct Int value`() {
|
||||
val map = mapOf("key" to "123")
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertEquals(123, provider.getInt("key"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getInt returns null for non-integer String value`() {
|
||||
val map = mapOf("key" to "notAnInt")
|
||||
val provider = MapConfigFieldProvider(map)
|
||||
assertThrows(NumberFormatException::class.java) {
|
||||
provider.getInt("key")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.configuration
|
||||
|
||||
import me.proton.core.configuration.provider.StaticClassConfigFieldProvider
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
class StaticClassConfigFieldProviderTest {
|
||||
@Test
|
||||
fun `getString returns correct String value`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertEquals("stringValue", provider.getString("stringKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBoolean returns correct Boolean value`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertTrue(provider.getBoolean("booleanKey")!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getInt returns correct Int value`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertEquals(123, provider.getInt("intKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getString returns null for non-existent key`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertNull(provider.getString("nonExistentKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBoolean returns null for non-existent key`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertNull(provider.getBoolean("nonExistentKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getInt returns null for non-existent key`() {
|
||||
val provider = StaticClassConfigFieldProvider(TestClass::class.java.name)
|
||||
assertNull(provider.getInt("nonExistentKey"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `throws exception for non-existent class`() {
|
||||
val nonExistentClassName = "com.example.NonExistentClass"
|
||||
assertThrows(IllegalStateException::class.java) {
|
||||
StaticClassConfigFieldProvider(nonExistentClassName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestClass {
|
||||
val stringKey: String = "stringValue"
|
||||
val booleanKey: Boolean = true
|
||||
val intKey: Int = 123
|
||||
}
|
|
@ -1,9 +1,17 @@
|
|||
public class dagger/hilt/internal/aggregatedroot/codegen/_me_proton_core_test_rule_EnvironmentConfigRule {
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public class hilt_aggregated_deps/_me_proton_core_test_rule_EnvironmentConfigRule_GeneratedInjector {
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public class hilt_aggregated_deps/_me_proton_core_test_rule_di_TestEnvironmentConfigModule {
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/AuthenticationRule : org/junit/rules/TestWatcher {
|
||||
public fun <init> (Lme/proton/core/test/rule/ProtonRule$UserConfig;)V
|
||||
public fun <init> (Lkotlin/jvm/functions/Function0;)V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/BuildConfig {
|
||||
|
@ -13,70 +21,41 @@ public final class me/proton/core/test/rule/BuildConfig {
|
|||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/EnvironmentConfigRule : org/junit/rules/TestRule {
|
||||
public fun <init> ()V
|
||||
public final class me/proton/core/test/rule/EnvironmentConfigRule : org/junit/rules/TestWatcher {
|
||||
public fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;)V
|
||||
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
|
||||
public final fun getConfig ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
|
||||
public fun starting (Lorg/junit/runner/Description;)V
|
||||
}
|
||||
|
||||
public abstract interface class me/proton/core/test/rule/EnvironmentConfigRule_GeneratedInjector {
|
||||
public abstract fun injectTest (Lme/proton/core/test/rule/EnvironmentConfigRule;)V
|
||||
}
|
||||
|
||||
public class me/proton/core/test/rule/ProtonRule : org/junit/rules/TestRule {
|
||||
public fun <init> (Lme/proton/core/test/rule/ProtonRule$UserConfig;Lme/proton/core/test/rule/ProtonRule$TestConfig;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
|
||||
public fun <init> (Lme/proton/core/test/rule/entity/UserConfig;Lme/proton/core/test/rule/entity/TestConfig;Lme/proton/core/test/rule/entity/HiltConfig;)V
|
||||
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
|
||||
public final fun getActivityScenarioRule ()Lorg/junit/rules/TestRule;
|
||||
public final fun getTestDataRule ()Lme/proton/core/test/rule/QuarkTestDataRule;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/ProtonRule$TestConfig {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;)V
|
||||
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
|
||||
public final fun component2 ()[Lme/proton/core/test/rule/annotation/AnnotationTestData;
|
||||
public final fun component3 ()Lorg/junit/rules/TestRule;
|
||||
public final fun copy (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;)Lme/proton/core/test/rule/ProtonRule$TestConfig;
|
||||
public static synthetic fun copy$default (Lme/proton/core/test/rule/ProtonRule$TestConfig;Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;ILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule$TestConfig;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getActivityRule ()Lorg/junit/rules/TestRule;
|
||||
public final fun getAnnotationTestData ()[Lme/proton/core/test/rule/annotation/AnnotationTestData;
|
||||
public final fun getEnvConfig ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/ProtonRule$UserConfig {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)V
|
||||
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public final fun component2 ()Z
|
||||
public final fun component3 ()Z
|
||||
public final fun component4 ()Z
|
||||
public final fun copy (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)Lme/proton/core/test/rule/ProtonRule$UserConfig;
|
||||
public static synthetic fun copy$default (Lme/proton/core/test/rule/ProtonRule$UserConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule$UserConfig;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getLoginBefore ()Z
|
||||
public final fun getLogoutAfter ()Z
|
||||
public final fun getLogoutBefore ()Z
|
||||
public final fun getOverrideLogin ()Z
|
||||
public final fun getUserData ()Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/ProtonRuleKt {
|
||||
public static final fun before (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Lorg/junit/rules/ExternalResource;
|
||||
public static final fun printInfo (Ljava/lang/Object;Ljava/lang/String;)V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/QuarkTestDataRule : org/junit/rules/TestWatcher {
|
||||
public static final field Companion Lme/proton/core/test/rule/QuarkTestDataRule$Companion;
|
||||
public fun <init> ([Lme/proton/core/test/rule/annotation/AnnotationTestData;Lme/proton/core/test/rule/annotation/TestUserData;Lkotlin/jvm/functions/Function0;)V
|
||||
public fun <init> (Ljava/util/Set;Lme/proton/core/test/rule/annotation/TestUserData;Lkotlin/jvm/functions/Function0;)V
|
||||
public final fun getTestData (Ljava/lang/Class;)Ljava/lang/annotation/Annotation;
|
||||
public final fun getTestUserData ()Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public final fun setTestUserData (Lme/proton/core/test/rule/annotation/TestUserData;)V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/QuarkTestDataRule$Companion {
|
||||
public final fun getQuarkClientTimeout ()Ljava/util/concurrent/atomic/AtomicReference;
|
||||
public final fun getQuarkClient ()Lokhttp3/OkHttpClient;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/TestExecutionWatcher : org/junit/rules/TestWatcher {
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/annotation/AnnotationTestData {
|
||||
|
@ -86,8 +65,8 @@ public final class me/proton/core/test/rule/annotation/AnnotationTestData {
|
|||
public synthetic fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;)V
|
||||
public synthetic fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun getDefault ()Ljava/lang/annotation/Annotation;
|
||||
public final fun getImplementation ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getInstance ()Ljava/lang/annotation/Annotation;
|
||||
public final fun getTearDown ()Lkotlin/jvm/functions/Function4;
|
||||
}
|
||||
|
||||
|
@ -130,6 +109,7 @@ public abstract interface annotation class me/proton/core/test/rule/annotation/T
|
|||
public static final field Companion Lme/proton/core/test/rule/annotation/TestUserData$Companion;
|
||||
public abstract fun authVersion ()I
|
||||
public abstract fun createAddress ()Z
|
||||
public abstract fun creationTime ()Ljava/lang/String;
|
||||
public abstract fun external ()Z
|
||||
public abstract fun externalEmail ()Ljava/lang/String;
|
||||
public abstract fun genKeys ()Lme/proton/core/test/rule/annotation/TestUserData$GenKeys;
|
||||
|
@ -141,6 +121,7 @@ public abstract interface annotation class me/proton/core/test/rule/annotation/T
|
|||
public abstract fun shouldSeed ()Z
|
||||
public abstract fun status ()Lme/proton/core/test/rule/annotation/TestUserData$Status;
|
||||
public abstract fun toTpSecret ()Ljava/lang/String;
|
||||
public abstract fun vpnSettings ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/annotation/TestUserData$Companion {
|
||||
|
@ -171,22 +152,75 @@ public final class me/proton/core/test/rule/annotation/TestUserData$Status : jav
|
|||
}
|
||||
|
||||
public final class me/proton/core/test/rule/annotation/TestUserDataKt {
|
||||
public static final fun handleExternal (Lme/proton/core/test/rule/annotation/TestUserData;Ljava/lang/String;Ljava/lang/String;)Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public static synthetic fun handleExternal$default (Lme/proton/core/test/rule/annotation/TestUserData;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public static final fun getAnnotationTestData (Lme/proton/core/test/rule/annotation/TestUserData;)Lme/proton/core/test/rule/annotation/AnnotationTestData;
|
||||
public static final fun getShouldHandleExternal (Lme/proton/core/test/rule/annotation/TestUserData;)Z
|
||||
public static final fun handleExternal (Lme/proton/core/test/rule/annotation/TestUserData;Ljava/lang/String;)Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public static synthetic fun handleExternal$default (Lme/proton/core/test/rule/annotation/TestUserData;Ljava/lang/String;ILjava/lang/Object;)Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/di/TestEnvironmentConfigModule {
|
||||
public static final field INSTANCE Lme/proton/core/test/rule/di/TestEnvironmentConfigModule;
|
||||
public final fun getOverrideConfig ()Ljava/util/concurrent/atomic/AtomicReference;
|
||||
public final fun provideEnvironmentConfiguration ()Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public final fun getOverrideEnvironmentConfiguration ()Ljava/util/concurrent/atomic/AtomicReference;
|
||||
public final fun provideEnvironmentConfiguration (Lme/proton/core/configuration/ContentResolverConfigManager;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/di/TestEnvironmentConfigModule_ProvideEnvironmentConfigurationFactory : dagger/internal/Factory {
|
||||
public fun <init> ()V
|
||||
public static fun create ()Lme/proton/core/test/rule/di/TestEnvironmentConfigModule_ProvideEnvironmentConfigurationFactory;
|
||||
public fun <init> (Ljavax/inject/Provider;)V
|
||||
public static fun create (Ljavax/inject/Provider;)Lme/proton/core/test/rule/di/TestEnvironmentConfigModule_ProvideEnvironmentConfigurationFactory;
|
||||
public synthetic fun get ()Ljava/lang/Object;
|
||||
public fun get ()Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public static fun provideEnvironmentConfiguration ()Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
public static fun provideEnvironmentConfiguration (Lme/proton/core/configuration/ContentResolverConfigManager;)Lme/proton/core/configuration/EnvironmentConfiguration;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/entity/HiltConfig {
|
||||
public fun <init> (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
|
||||
public final fun component1 ()Ljava/lang/Object;
|
||||
public final fun component2 ()Lkotlin/jvm/functions/Function1;
|
||||
public final fun component3 ()Lkotlin/jvm/functions/Function1;
|
||||
public final fun copy (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lme/proton/core/test/rule/entity/HiltConfig;
|
||||
public static synthetic fun copy$default (Lme/proton/core/test/rule/entity/HiltConfig;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/proton/core/test/rule/entity/HiltConfig;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getAfterHilt ()Lkotlin/jvm/functions/Function1;
|
||||
public final fun getBeforeHilt ()Lkotlin/jvm/functions/Function1;
|
||||
public final fun getHiltInstance ()Ljava/lang/Object;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/entity/TestConfig {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;Ljava/util/Set;Lorg/junit/rules/TestRule;)V
|
||||
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;Ljava/util/Set;Lorg/junit/rules/TestRule;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
|
||||
public final fun component2 ()Ljava/util/Set;
|
||||
public final fun component3 ()Lorg/junit/rules/TestRule;
|
||||
public final fun copy (Lme/proton/core/test/rule/annotation/EnvironmentConfig;Ljava/util/Set;Lorg/junit/rules/TestRule;)Lme/proton/core/test/rule/entity/TestConfig;
|
||||
public static synthetic fun copy$default (Lme/proton/core/test/rule/entity/TestConfig;Lme/proton/core/test/rule/annotation/EnvironmentConfig;Ljava/util/Set;Lorg/junit/rules/TestRule;ILjava/lang/Object;)Lme/proton/core/test/rule/entity/TestConfig;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getActivityRule ()Lorg/junit/rules/TestRule;
|
||||
public final fun getAnnotationTestData ()Ljava/util/Set;
|
||||
public final fun getEnvConfig ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/entity/UserConfig {
|
||||
public fun <init> ()V
|
||||
public fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)V
|
||||
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun component1 ()Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public final fun component2 ()Z
|
||||
public final fun component3 ()Z
|
||||
public final fun component4 ()Z
|
||||
public final fun copy (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)Lme/proton/core/test/rule/entity/UserConfig;
|
||||
public static synthetic fun copy$default (Lme/proton/core/test/rule/entity/UserConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZILjava/lang/Object;)Lme/proton/core/test/rule/entity/UserConfig;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getLoginBefore ()Z
|
||||
public final fun getLogoutAfter ()Z
|
||||
public final fun getLogoutBefore ()Z
|
||||
public final fun getUserData ()Lme/proton/core/test/rule/annotation/TestUserData;
|
||||
public fun hashCode ()I
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class me/proton/core/test/rule/extension/QuarkCommandKt {
|
||||
|
@ -196,7 +230,7 @@ public final class me/proton/core/test/rule/extension/QuarkCommandKt {
|
|||
}
|
||||
|
||||
public final class me/proton/core/test/rule/extension/TestRuleKt {
|
||||
public static final fun protonRule (Ljava/lang/Object;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lme/proton/core/test/rule/annotation/EnvironmentConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZLorg/junit/rules/TestRule;Lkotlin/jvm/functions/Function0;)Lme/proton/core/test/rule/ProtonRule;
|
||||
public static synthetic fun protonRule$default (Ljava/lang/Object;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lme/proton/core/test/rule/annotation/EnvironmentConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZLorg/junit/rules/TestRule;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule;
|
||||
public static final fun protonRule (Ljava/lang/Object;Ljava/util/Set;Lme/proton/core/test/rule/annotation/EnvironmentConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZLorg/junit/rules/TestRule;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lme/proton/core/test/rule/ProtonRule;
|
||||
public static synthetic fun protonRule$default (Ljava/lang/Object;Ljava/util/Set;Lme/proton/core/test/rule/annotation/EnvironmentConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZLorg/junit/rules/TestRule;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,10 @@ import androidx.test.core.app.ApplicationProvider
|
|||
import dagger.hilt.android.EntryPointAccessors
|
||||
import me.proton.core.auth.domain.testing.LoginTestHelper
|
||||
import me.proton.core.auth.presentation.testing.ProtonTestEntryPoint
|
||||
import me.proton.core.test.rule.entity.UserConfig
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import kotlin.time.measureTime
|
||||
|
||||
/**
|
||||
* A JUnit test rule for managing user authentication states before and after tests.
|
||||
|
@ -34,9 +36,11 @@ import org.junit.runner.Description
|
|||
*/
|
||||
@SuppressLint("RestrictedApi")
|
||||
public class AuthenticationRule(
|
||||
private val userConfig: ProtonRule.UserConfig,
|
||||
config: () -> UserConfig,
|
||||
) : TestWatcher() {
|
||||
|
||||
private val userConfig by lazy(config)
|
||||
|
||||
private val loginTestHelper: LoginTestHelper by lazy {
|
||||
protonTestEntryPoint.loginTestHelper
|
||||
}
|
||||
|
@ -54,8 +58,15 @@ public class AuthenticationRule(
|
|||
}
|
||||
|
||||
userConfig.userData?.let {
|
||||
if (userConfig.loginBefore)
|
||||
loginTestHelper.login(it.name, it.password)
|
||||
if (userConfig.loginBefore) {
|
||||
printInfo("Logging in: ${it.name} / ${it.password} ...")
|
||||
|
||||
val loginTime = measureTime {
|
||||
loginTestHelper.login(it.name, it.password)
|
||||
}
|
||||
|
||||
printInfo("Logged in in ${loginTime.inWholeSeconds} seconds.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,6 +77,7 @@ public class AuthenticationRule(
|
|||
}
|
||||
|
||||
private fun logout() {
|
||||
printInfo("Logging out all users")
|
||||
runCatching { loginTestHelper.logoutAll() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,41 +18,32 @@
|
|||
|
||||
package me.proton.core.test.rule
|
||||
|
||||
import me.proton.core.configuration.EnvironmentConfiguration
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
import me.proton.core.test.rule.annotation.EnvironmentConfig
|
||||
import me.proton.core.test.rule.annotation.configContractFieldsMap
|
||||
import me.proton.core.test.rule.di.TestEnvironmentConfigModule.overrideConfig
|
||||
import me.proton.core.test.rule.di.TestEnvironmentConfigModule.provideEnvironmentConfiguration
|
||||
import org.junit.rules.TestRule
|
||||
import me.proton.core.test.rule.annotation.toEnvironmentConfiguration
|
||||
import me.proton.core.test.rule.di.TestEnvironmentConfigModule.overrideEnvironmentConfiguration
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
|
||||
/**
|
||||
* A test rule for setting up environment configuration before running tests.
|
||||
*
|
||||
* @property defaultConfig The default [EnvironmentConfig] to use for tests if no overrides are specified.
|
||||
* By default, it uses a configuration provided by [provideEnvironmentConfiguration].
|
||||
* @property ruleConfig The default [EnvironmentConfig] to use for tests if no overrides are specified.
|
||||
*/
|
||||
@HiltAndroidTest
|
||||
public class EnvironmentConfigRule(
|
||||
private val defaultConfig: EnvironmentConfig =
|
||||
EnvironmentConfig.fromConfiguration(provideEnvironmentConfiguration())
|
||||
) : TestRule {
|
||||
private val ruleConfig: EnvironmentConfig?
|
||||
) : TestWatcher() {
|
||||
public override fun starting(description: Description) {
|
||||
val annotationConfig = description.getAnnotation(EnvironmentConfig::class.java)
|
||||
val overrideConfig = annotationConfig ?: ruleConfig ?: return
|
||||
val overrideEnvironmentConfig = overrideConfig.toEnvironmentConfiguration()
|
||||
|
||||
/**
|
||||
* The active [EnvironmentConfig] for the current test. It is determined by looking for an
|
||||
* [EnvironmentConfig] annotation on the test method or class. If not found, [defaultConfig] is used.
|
||||
*
|
||||
* This property is initialized when the rule is applied and is accessible during the test execution.
|
||||
*/
|
||||
public lateinit var config: EnvironmentConfig
|
||||
private set
|
||||
overrideEnvironmentConfiguration.set(overrideEnvironmentConfig)
|
||||
|
||||
/**
|
||||
* Applies the environment configuration for the test described by [description].
|
||||
*/
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
config = description.getAnnotation(EnvironmentConfig::class.java) ?: defaultConfig
|
||||
EnvironmentConfiguration.fromMap(config.configContractFieldsMap).apply(overrideConfig::set)
|
||||
return base
|
||||
val overrideString = if (annotationConfig != null) "@EnvironmentConfig annotation" else "ProtonRule argument"
|
||||
|
||||
printInfo("Overriding EnvironmentConfiguration with $overrideString: ${overrideEnvironmentConfig.primitiveFieldMap}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,12 +18,15 @@
|
|||
|
||||
package me.proton.core.test.rule
|
||||
|
||||
import android.util.Log
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.proton.core.test.rule.annotation.AnnotationTestData
|
||||
import me.proton.core.test.rule.annotation.EnvironmentConfig
|
||||
import me.proton.core.test.rule.annotation.TestUserData
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.test.rule.di.TestEnvironmentConfigModule.provideEnvironmentConfiguration
|
||||
import me.proton.core.test.rule.entity.HiltConfig
|
||||
import me.proton.core.test.rule.entity.TestConfig
|
||||
import me.proton.core.test.rule.entity.UserConfig
|
||||
import org.junit.rules.ExternalResource
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.TestRule
|
||||
|
@ -44,74 +47,91 @@ import org.junit.runners.model.Statement
|
|||
*
|
||||
* @param userConfig Configuration for user data and related behavior (e.g., login/logout settings).
|
||||
* @param testConfig Configuration for environment, test data, and an optional activity rule.
|
||||
* @param hiltTestInstance Hilt test instance for dependency injection.
|
||||
* @param setup A lambda function containing setup logic to be executed before each test.
|
||||
* @param hiltConfig Configuration for hooking into hilt setup
|
||||
*/
|
||||
public open class ProtonRule(
|
||||
private val userConfig: UserConfig,
|
||||
private val userConfig: UserConfig?,
|
||||
private val testConfig: TestConfig,
|
||||
private val hiltTestInstance: Any,
|
||||
private val setup: () -> Any
|
||||
private val hiltConfig: HiltConfig?,
|
||||
) : TestRule {
|
||||
|
||||
public val activityScenarioRule: TestRule? = testConfig.activityRule
|
||||
|
||||
private val targetContext get() = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
private val environmentConfigRule by lazy {
|
||||
val envConfig = testConfig.envConfig ?: EnvironmentConfig.fromConfiguration(provideEnvironmentConfiguration())
|
||||
EnvironmentConfigRule(envConfig)
|
||||
EnvironmentConfigRule(testConfig.envConfig)
|
||||
}
|
||||
|
||||
private val hiltRule by lazy {
|
||||
HiltAndroidRule(hiltTestInstance)
|
||||
private val hiltRule: HiltAndroidRule? by lazy {
|
||||
HiltAndroidRule(hiltConfig?.hiltInstance ?: return@lazy null)
|
||||
}
|
||||
|
||||
public val testDataRule: QuarkTestDataRule by lazy {
|
||||
private val hiltInjectRule by lazy {
|
||||
if (hiltConfig == null) return@lazy null
|
||||
before {
|
||||
hiltRule!!.inject()
|
||||
}
|
||||
}
|
||||
|
||||
public val testDataRule: QuarkTestDataRule? by lazy {
|
||||
if (userConfig?.userData == null && testConfig.annotationTestData.isEmpty()) return@lazy null
|
||||
QuarkTestDataRule(
|
||||
*testConfig.annotationTestData,
|
||||
initialTestUserData = userConfig.userData,
|
||||
environmentConfig = { environmentConfigRule.config }
|
||||
testConfig.annotationTestData,
|
||||
initialTestUserData = userConfig?.userData,
|
||||
environmentConfiguration = {
|
||||
provideEnvironmentConfiguration(ContentResolverConfigManager(targetContext))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private val authenticationRule by lazy {
|
||||
userConfig
|
||||
.takeUnless { it.userData == null }
|
||||
?.let {
|
||||
AuthenticationRule(it)
|
||||
}
|
||||
if (userConfig == null) return@lazy null.also {
|
||||
printInfo("No UserConfig provided. Skipping authentication.")
|
||||
}
|
||||
AuthenticationRule {
|
||||
UserConfig(
|
||||
testDataRule?.testUserData,
|
||||
loginBefore = userConfig.loginBefore,
|
||||
logoutBefore = userConfig.logoutBefore,
|
||||
logoutAfter = userConfig.logoutAfter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val setupRule by lazy {
|
||||
before { setup() }
|
||||
private val beforeHiltRule by lazy {
|
||||
if (hiltConfig?.beforeHilt == null) return@lazy null
|
||||
before {
|
||||
printInfo("Executing beforeHilt()")
|
||||
hiltConfig!!.beforeHilt.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
private val ruleChain: RuleChain by lazy {
|
||||
RuleChain.outerRule(environmentConfigRule)
|
||||
.around(hiltRule)
|
||||
.around(setupRule)
|
||||
.around(testDataRule)
|
||||
private val afterHiltRule by lazy {
|
||||
if (hiltConfig?.afterHilt == null) return@lazy null
|
||||
before {
|
||||
printInfo("Executing afterHilt()")
|
||||
hiltConfig!!.afterHilt.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement {
|
||||
return RuleChain
|
||||
.outerRule(beforeHiltRule)
|
||||
.aroundNullable(hiltRule)
|
||||
.around(environmentConfigRule)
|
||||
.around(hiltInjectRule)
|
||||
.aroundNullable(afterHiltRule)
|
||||
.aroundNullable(testDataRule)
|
||||
.aroundNullable(authenticationRule)
|
||||
.aroundNullable(testConfig.activityRule)
|
||||
.around(TestExecutionWatcher())
|
||||
.apply(base, description)
|
||||
}
|
||||
}
|
||||
|
||||
override fun apply(base: Statement, description: Description): Statement = ruleChain.apply(base, description)
|
||||
|
||||
public data class UserConfig(
|
||||
val userData: TestUserData? = null,
|
||||
val loginBefore: Boolean = true,
|
||||
val logoutBefore: Boolean = true,
|
||||
val logoutAfter: Boolean = true
|
||||
) {
|
||||
val overrideLogin: Boolean get() = loginBefore || logoutBefore || logoutAfter
|
||||
}
|
||||
|
||||
public data class TestConfig(
|
||||
val envConfig: EnvironmentConfig? = null,
|
||||
val annotationTestData: Array<out AnnotationTestData<Annotation>> = emptyArray(),
|
||||
val activityRule: TestRule? = null,
|
||||
)
|
||||
|
||||
private fun RuleChain.aroundNullable(rule: TestRule?): RuleChain {
|
||||
return around(rule ?: return this)
|
||||
}
|
||||
private fun RuleChain.aroundNullable(rule: TestRule?): RuleChain {
|
||||
return around(rule ?: return this)
|
||||
}
|
||||
|
||||
public fun <T> T.before(block: suspend T.() -> Any): ExternalResource =
|
||||
|
@ -122,3 +142,11 @@ public fun <T> T.before(block: suspend T.() -> Any): ExternalResource =
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun Any.printInfo(message: String) {
|
||||
val (tag, msg) = this::class.java.name to "[ProtonRule] -> $message"
|
||||
if (message.contains("CRITICAL") || message.contains("failed!"))
|
||||
Log.e(tag, msg)
|
||||
else
|
||||
Log.i(tag, msg)
|
||||
}
|
||||
|
|
|
@ -24,15 +24,17 @@ import me.proton.core.test.quark.v2.QuarkCommand
|
|||
import me.proton.core.test.rule.annotation.AnnotationTestData
|
||||
import me.proton.core.test.rule.annotation.EnvironmentConfig
|
||||
import me.proton.core.test.rule.annotation.TestUserData
|
||||
import me.proton.core.test.rule.annotation.annotationTestData
|
||||
import me.proton.core.test.rule.annotation.handleExternal
|
||||
import me.proton.core.test.rule.annotation.toEnvironmentConfiguration
|
||||
import me.proton.core.test.rule.extension.seedTestUserData
|
||||
import me.proton.core.test.rule.annotation.shouldHandleExternal
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.measureTime
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
/**
|
||||
|
@ -42,20 +44,16 @@ import kotlin.time.toJavaDuration
|
|||
*
|
||||
* @property initialTestUserData Initial user data to set up before tests. Can be overridden by test-specific
|
||||
* annotations.
|
||||
* @property environmentConfig A lambda expression that supplies the [EnvironmentConfig] used for test execution.
|
||||
* @property environmentConfiguration A lambda expression that supplies the [EnvironmentConfig] used for test execution.
|
||||
*/
|
||||
public class QuarkTestDataRule(
|
||||
private vararg val annotationTestData: AnnotationTestData<Annotation>,
|
||||
initialTestUserData: TestUserData?,
|
||||
private val environmentConfig: () -> EnvironmentConfig
|
||||
private val annotationTestData: Set<AnnotationTestData<Annotation>>,
|
||||
private val initialTestUserData: TestUserData?,
|
||||
private val environmentConfiguration: () -> EnvironmentConfiguration
|
||||
) : TestWatcher() {
|
||||
|
||||
private lateinit var quarkCommand: QuarkCommand
|
||||
|
||||
private val environmentConfiguration: EnvironmentConfiguration by lazy {
|
||||
environmentConfig().toEnvironmentConfiguration()
|
||||
}
|
||||
|
||||
private var createdUserResponse: CreateUserQuarkResponse? = null
|
||||
|
||||
private val testDataMap: MutableMap<Class<out Annotation>, Annotation> = mutableMapOf()
|
||||
|
@ -64,41 +62,95 @@ public class QuarkTestDataRule(
|
|||
* The user data applied to the current test, initially set from [initialTestUserData] and potentially
|
||||
* overridden by test-specific annotations.
|
||||
*/
|
||||
public var testUserData: TestUserData? = initialTestUserData?.handleExternal()
|
||||
private set
|
||||
public var testUserData: TestUserData? = null
|
||||
|
||||
/**
|
||||
* Prepares and applies test data and configurations before each test starts. This includes setting up
|
||||
* user/env data and processing all test data entries against the test's annotations.
|
||||
*/
|
||||
override fun starting(description: Description) {
|
||||
quarkCommand = QuarkCommand(quarkClient)
|
||||
.baseUrl("https://${environmentConfiguration.host}/api/internal")
|
||||
.proxyToken(environmentConfiguration.proxyToken)
|
||||
quarkCommand = getQuarkCommand(environmentConfiguration())
|
||||
|
||||
testUserData = description.getAnnotation(TestUserData::class.java)?.handleExternal() ?: testUserData
|
||||
if (initialTestUserData != null) {
|
||||
val userData = description.getAnnotation(TestUserData::class.java) ?: initialTestUserData
|
||||
val handledUserData = userData.handleExternal()
|
||||
|
||||
testUserData?.takeIf { it.shouldSeed }?.apply {
|
||||
createdUserResponse = quarkCommand.seedTestUserData(this)
|
||||
if (userData.shouldHandleExternal) {
|
||||
printInfo("isExternal set to true, but external email is empty. Overriding values with name: ${handledUserData.name}, externalEmail: ${handledUserData.externalEmail}")
|
||||
}
|
||||
|
||||
handledUserData.annotationTestData.let {
|
||||
val userSeedingTime = measureTime {
|
||||
createdUserResponse = it.implementation(
|
||||
quarkCommand,
|
||||
it.instance,
|
||||
null,
|
||||
null
|
||||
) as CreateUserQuarkResponse
|
||||
}
|
||||
|
||||
printInfo("${TestUserData::class.java.simpleName} seeding done: $createdUserResponse")
|
||||
printInfo("${TestUserData::class.java.simpleName} seeded in ${userSeedingTime.inWholeSeconds} seconds")
|
||||
|
||||
testUserData = it.instance
|
||||
testDataMap[it.annotationClass] = it.instance
|
||||
|
||||
printInfo("Running Test with $testUserData")
|
||||
}
|
||||
}
|
||||
|
||||
annotationTestData.forEach { entry ->
|
||||
val annotationData = description.getAnnotation(entry.annotationClass) ?: entry.default
|
||||
entry.implementation(quarkCommand, annotationData, testUserData, createdUserResponse)
|
||||
testDataMap[entry.annotationClass] = annotationData
|
||||
val annotationData = getRuntimeAnnotationData(description, entry)
|
||||
|
||||
val seedingTime = measureTime {
|
||||
val result = annotationData.implementation(
|
||||
quarkCommand,
|
||||
entry.instance,
|
||||
testUserData,
|
||||
createdUserResponse
|
||||
)
|
||||
|
||||
if (result is Response) {
|
||||
printInfo("Seeding response received: { ${result.message} }")
|
||||
}
|
||||
|
||||
printInfo("Running Test with ${entry.instance}")
|
||||
}
|
||||
|
||||
printInfo("${entry.annotationClass.simpleName} data seeded in ${seedingTime.inWholeSeconds} seconds")
|
||||
|
||||
testDataMap[entry.annotationClass] = annotationData.instance
|
||||
}
|
||||
}
|
||||
|
||||
private fun getQuarkCommand(envConfig: EnvironmentConfiguration): QuarkCommand =
|
||||
QuarkCommand(quarkClient)
|
||||
.baseUrl("https://${envConfig.host}/api/internal")
|
||||
.proxyToken(envConfig.proxyToken)
|
||||
|
||||
private inline fun <reified T : Annotation> getRuntimeAnnotationData(
|
||||
description: Description,
|
||||
defaultData: AnnotationTestData<T>
|
||||
): AnnotationTestData<T> = AnnotationTestData(
|
||||
description.getAnnotation(T::class.java) ?: defaultData.instance,
|
||||
defaultData.implementation,
|
||||
defaultData.tearDown
|
||||
)
|
||||
|
||||
/** Clean up test data and configurations after each test finishes. **/
|
||||
override fun finished(description: Description) {
|
||||
annotationTestData.forEach { data ->
|
||||
testDataMap[data.annotationClass]?.let {
|
||||
data.tearDown?.invoke(
|
||||
annotationTestData.forEach { entry ->
|
||||
testDataMap[entry.annotationClass]?.let {
|
||||
val result = entry.tearDown?.invoke(
|
||||
quarkCommand,
|
||||
it,
|
||||
testUserData,
|
||||
createdUserResponse
|
||||
)
|
||||
) ?: return@let
|
||||
|
||||
if (result is Response) {
|
||||
printInfo("Tear down response received: { ${result.message} }")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -108,12 +160,12 @@ public class QuarkTestDataRule(
|
|||
public fun <T : Annotation> getTestData(annotationClass: Class<T>): T = testDataMap[annotationClass] as T
|
||||
|
||||
private val AnnotationTestData<Annotation>.annotationClass: Class<out Annotation>
|
||||
get() = default.annotationClass.java
|
||||
get() = instance.annotationClass.java
|
||||
|
||||
public companion object {
|
||||
public val quarkClientTimeout: AtomicReference<Duration> = AtomicReference(60.seconds)
|
||||
private val quarkClientTimeout: AtomicReference<Duration> = AtomicReference(60.seconds)
|
||||
|
||||
private val quarkClient: OkHttpClient by lazy {
|
||||
public val quarkClient: OkHttpClient by lazy {
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(quarkClientTimeout.get().toJavaDuration())
|
||||
.readTimeout(quarkClientTimeout.get().toJavaDuration())
|
||||
|
@ -121,4 +173,4 @@ public class QuarkTestDataRule(
|
|||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.test.rule
|
||||
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
|
||||
public class TestExecutionWatcher: TestWatcher() {
|
||||
override fun starting(description: Description?) {
|
||||
printInfo("${description?.methodName} starting")
|
||||
}
|
||||
|
||||
override fun finished(description: Description?) {
|
||||
printInfo("${description?.methodName} finished")
|
||||
}
|
||||
|
||||
override fun failed(e: Throwable?, description: Description?) {
|
||||
printInfo("${description?.methodName} failed! Exception: ${e!!::class.java.simpleName}")
|
||||
}
|
||||
|
||||
override fun succeeded(description: Description?) {
|
||||
printInfo("${description?.methodName} succeeded!")
|
||||
}
|
||||
}
|
|
@ -25,17 +25,17 @@ import me.proton.core.test.quark.v2.QuarkCommand
|
|||
* Represents a test data configuration for the `QuarkTestDataRule`.
|
||||
*
|
||||
* @param T The annotation type associated with this test data configuration.
|
||||
* @param default The default annotation instance to use if no test-specific annotation is present.
|
||||
* @param instance The default annotation instance to use if no test-specific annotation is present.
|
||||
* @param implementation A lambda function defining how to apply the test data using the provided Quark command,
|
||||
* test user data (if available), and created user response (if available).
|
||||
* @param tearDown An optional lambda function defining how to tear down the test data after the test, using the
|
||||
* provided Quark command, test data annotation, test user data (if available),
|
||||
* and created user response (if available).
|
||||
*/
|
||||
public class AnnotationTestData<T: Annotation>(
|
||||
public val default: T,
|
||||
public val implementation: QuarkCommand.(T, TestUserData?, CreateUserQuarkResponse?) -> Any,
|
||||
public val tearDown: (QuarkCommand.(T, TestUserData?, CreateUserQuarkResponse?) -> Unit)? = null,
|
||||
public class AnnotationTestData<out T: Annotation>(
|
||||
public val instance: T,
|
||||
public val implementation: QuarkCommand.(@UnsafeVariance T, TestUserData?, CreateUserQuarkResponse?) -> Any,
|
||||
public val tearDown: (QuarkCommand.(@UnsafeVariance T, TestUserData?, CreateUserQuarkResponse?) -> Any?)? = null,
|
||||
) {
|
||||
|
||||
/**
|
||||
|
@ -50,9 +50,9 @@ public class AnnotationTestData<T: Annotation>(
|
|||
public constructor(
|
||||
default: T,
|
||||
implementation: QuarkCommand.(T) -> Any,
|
||||
tearDown: (QuarkCommand.(T) -> Unit)? = null
|
||||
tearDown: (QuarkCommand.(T) -> Any)? = null
|
||||
) : this(
|
||||
default = default,
|
||||
instance = default,
|
||||
implementation = { data, _, _ -> implementation(data) },
|
||||
tearDown = { data, _, _ -> tearDown?.invoke(this, data) }
|
||||
)
|
||||
|
@ -71,7 +71,7 @@ public class AnnotationTestData<T: Annotation>(
|
|||
implementation: QuarkCommand.(T, CreateUserQuarkResponse) -> Any,
|
||||
tearDown: (QuarkCommand.(T, CreateUserQuarkResponse) -> Unit)? = null
|
||||
) : this(
|
||||
default = default,
|
||||
instance = default,
|
||||
implementation = { data, _, seededUser ->
|
||||
implementation(
|
||||
data,
|
||||
|
@ -86,4 +86,4 @@ public class AnnotationTestData<T: Annotation>(
|
|||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,4 +36,4 @@ public val EnvironmentConfig.configContractFieldsMap: Map<String, String?>
|
|||
get() = mapOf(::host.name to host)
|
||||
|
||||
public fun EnvironmentConfig.toEnvironmentConfiguration(): EnvironmentConfiguration =
|
||||
EnvironmentConfiguration.fromMap(configContractFieldsMap)
|
||||
EnvironmentConfiguration.fromMap(configContractFieldsMap)
|
||||
|
|
|
@ -26,7 +26,7 @@ import me.proton.core.util.kotlin.EMPTY_STRING
|
|||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
public annotation class TestSubscriptionData(
|
||||
val plan: Plan = Plan.Free,
|
||||
val plan: Plan,
|
||||
val couponCode: String = EMPTY_STRING,
|
||||
val delinquent: Boolean = false
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
package me.proton.core.test.rule.annotation
|
||||
|
||||
import me.proton.core.test.rule.extension.seedTestUserData
|
||||
import me.proton.core.util.kotlin.EMPTY_STRING
|
||||
import me.proton.core.util.kotlin.random
|
||||
|
||||
|
@ -37,6 +38,8 @@ public annotation class TestUserData(
|
|||
val toTpSecret: String = EMPTY_STRING,
|
||||
val recoveryPhone: String = EMPTY_STRING,
|
||||
val externalEmail: String = EMPTY_STRING,
|
||||
val vpnSettings: String = EMPTY_STRING,
|
||||
val creationTime: String = EMPTY_STRING,
|
||||
|
||||
val shouldSeed: Boolean = true,
|
||||
) {
|
||||
|
@ -59,10 +62,9 @@ public annotation class TestUserData(
|
|||
}
|
||||
|
||||
public fun TestUserData.handleExternal(
|
||||
username: String = TestUserData.randomUsername(),
|
||||
extEmail: String = "${TestUserData.randomUsername()}@example.lt"
|
||||
username: String = TestUserData.randomUsername()
|
||||
): TestUserData = TestUserData(
|
||||
name = if (name.isEmpty() && external) name else username,
|
||||
name = if (shouldHandleExternal) username else name,
|
||||
password,
|
||||
recoveryEmail,
|
||||
status,
|
||||
|
@ -73,5 +75,16 @@ public fun TestUserData.handleExternal(
|
|||
external,
|
||||
toTpSecret,
|
||||
recoveryPhone,
|
||||
externalEmail = if (externalEmail.isEmpty() && external) extEmail else externalEmail
|
||||
externalEmail = if (shouldHandleExternal) "$username@example.lt" else externalEmail
|
||||
)
|
||||
|
||||
public val TestUserData.annotationTestData: AnnotationTestData<TestUserData>
|
||||
get() = AnnotationTestData(
|
||||
default = this,
|
||||
implementation = { data ->
|
||||
seedTestUserData(data)
|
||||
}
|
||||
)
|
||||
|
||||
public val TestUserData.shouldHandleExternal: Boolean
|
||||
get() = externalEmail.isEmpty() && external
|
||||
|
|
|
@ -18,15 +18,17 @@
|
|||
|
||||
package me.proton.core.test.rule.di
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import me.proton.core.configuration.ContentResolverConfigManager
|
||||
import me.proton.core.configuration.EnvironmentConfiguration
|
||||
import me.proton.core.configuration.dagger.ContentResolverEnvironmentConfigModule
|
||||
import me.proton.core.configuration.entity.ConfigContract
|
||||
import me.proton.core.configuration.extension.primitiveFieldMap
|
||||
import me.proton.core.test.rule.printInfo
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -36,34 +38,34 @@ import javax.inject.Singleton
|
|||
replaces = [ContentResolverEnvironmentConfigModule::class]
|
||||
)
|
||||
public object TestEnvironmentConfigModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
public fun provideEnvironmentConfiguration(): EnvironmentConfiguration =
|
||||
|
||||
public val overrideEnvironmentConfiguration: AtomicReference<EnvironmentConfiguration?> = AtomicReference(null)
|
||||
|
||||
private val instrumentationArgumentsConfig by lazy {
|
||||
InstrumentationRegistry
|
||||
.getArguments()
|
||||
.configFields()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let {
|
||||
EnvironmentConfiguration.fromMap(it)
|
||||
} ?: EnvironmentConfiguration(::getConfigValue)
|
||||
|
||||
|
||||
public val overrideConfig: AtomicReference<EnvironmentConfiguration?> = AtomicReference(null)
|
||||
|
||||
private val defaultConfig = EnvironmentConfiguration.fromClass()
|
||||
|
||||
private fun getConfigValue(key: String): String {
|
||||
val defaultValue = defaultConfig.primitiveFieldMap[key].toString()
|
||||
val overrideValue = overrideConfig.get()?.primitiveFieldMap?.get(key)?.toString()
|
||||
return overrideValue ?: defaultValue
|
||||
.takeIf { it.containsKey(ConfigContract::host.name) || it.containsKey(ConfigContract::proxyToken.name) }
|
||||
?.let { args ->
|
||||
EnvironmentConfiguration.fromBundle(args).also {
|
||||
printInfo("Overriding EnvironmentConfiguration with Instrumentation arguments: ${it.primitiveFieldMap}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bundle.configFields(): Map<String, Any?> {
|
||||
val hostKey = EnvironmentConfiguration::host.name
|
||||
val proxyTokenKey = EnvironmentConfiguration::proxyToken.name
|
||||
return mapOf(
|
||||
hostKey to (getString(hostKey) ?: getConfigValue(hostKey)),
|
||||
proxyTokenKey to (getString(proxyTokenKey) ?: getConfigValue(proxyTokenKey))
|
||||
)
|
||||
private val staticEnvironmentConfig by lazy(EnvironmentConfiguration::fromClass)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public fun provideEnvironmentConfiguration(
|
||||
contentResolverConfigManager: ContentResolverConfigManager
|
||||
): EnvironmentConfiguration {
|
||||
val contentResolverConfig = contentResolverConfigManager
|
||||
.queryAtClassPath(EnvironmentConfiguration::class)
|
||||
?.let(EnvironmentConfiguration::fromMap)
|
||||
|
||||
return instrumentationArgumentsConfig
|
||||
?: overrideEnvironmentConfiguration.get()
|
||||
?: contentResolverConfig
|
||||
?: staticEnvironmentConfig
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.test.rule.entity
|
||||
|
||||
import me.proton.core.test.rule.ProtonRule
|
||||
|
||||
public data class HiltConfig(
|
||||
val hiltInstance: Any,
|
||||
val beforeHilt: (ProtonRule) -> Any,
|
||||
val afterHilt: (ProtonRule) -> Any
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.test.rule.entity
|
||||
|
||||
import me.proton.core.test.rule.annotation.AnnotationTestData
|
||||
import me.proton.core.test.rule.annotation.EnvironmentConfig
|
||||
import org.junit.rules.TestRule
|
||||
|
||||
public data class TestConfig(
|
||||
val envConfig: EnvironmentConfig? = null,
|
||||
val annotationTestData: Set<out AnnotationTestData<Annotation>> = emptySet(),
|
||||
val activityRule: TestRule? = null,
|
||||
)
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 Proton Technologies AG
|
||||
* This file is part of Proton AG and ProtonCore.
|
||||
*
|
||||
* ProtonCore is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* ProtonCore is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with ProtonCore. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.core.test.rule.entity
|
||||
|
||||
import me.proton.core.test.rule.annotation.TestUserData
|
||||
|
||||
public data class UserConfig(
|
||||
val userData: TestUserData? = null,
|
||||
val loginBefore: Boolean = true,
|
||||
val logoutBefore: Boolean = true,
|
||||
val logoutAfter: Boolean = true
|
||||
)
|
|
@ -41,7 +41,7 @@ public fun QuarkCommand.setPaymentMethods(methods: TestPaymentMethods): Response
|
|||
|
||||
public fun QuarkCommand.seedTestUserData(data: TestUserData): CreateUserQuarkResponse {
|
||||
val args = listOf(
|
||||
"-N" to data.name,
|
||||
"-N" to (data.name.takeIf { !data.external } ?: EMPTY_STRING),
|
||||
"-p" to data.password,
|
||||
"-c" to data.createAddress.trueOrEmpty(),
|
||||
"-r" to data.recoveryEmail,
|
||||
|
@ -51,9 +51,11 @@ public fun QuarkCommand.seedTestUserData(data: TestUserData): CreateUserQuarkRes
|
|||
"-k" to data.genKeys.valueOrEmpty(),
|
||||
"-m" to data.mailboxPassword,
|
||||
"-e" to data.external.trueOrEmpty(),
|
||||
"-ts" to data.toTpSecret,
|
||||
"-rp" to data.recoveryPhone,
|
||||
"-em" to data.externalEmail,
|
||||
"--vpn-settings" to data.vpnSettings,
|
||||
"--creation-time" to data.creationTime,
|
||||
"--totp-secret" to data.toTpSecret,
|
||||
"--recovery-phone" to data.recoveryPhone,
|
||||
"--external-email" to data.externalEmail,
|
||||
"--format" to "json"
|
||||
).toEncodedArgs(ignoreEmpty = true)
|
||||
|
||||
|
|
|
@ -28,6 +28,9 @@ import me.proton.core.test.rule.ProtonRule
|
|||
import me.proton.core.test.rule.annotation.AnnotationTestData
|
||||
import me.proton.core.test.rule.annotation.EnvironmentConfig
|
||||
import me.proton.core.test.rule.annotation.TestUserData
|
||||
import me.proton.core.test.rule.entity.HiltConfig
|
||||
import me.proton.core.test.rule.entity.TestConfig
|
||||
import me.proton.core.test.rule.entity.UserConfig
|
||||
import org.junit.rules.TestRule
|
||||
|
||||
/**
|
||||
|
@ -40,45 +43,51 @@ import org.junit.rules.TestRule
|
|||
* - Custom setup logic
|
||||
* - Activity or Compose test rule
|
||||
*
|
||||
* @param annotationTestData Array of `AnnotationTestData` for `QuarkTestDataRule`.
|
||||
* @param annotationTestData Set of `AnnotationTestData` for `QuarkTestDataRule`.
|
||||
* @param envConfig Environment configuration for the test (optional).
|
||||
* @param userData Test user data (optional).
|
||||
* @param loginBefore Whether to perform login before the test (default: false).
|
||||
* @param logoutBefore Whether to perform logout before the test (default: false).
|
||||
* @param logoutAfter Whether to perform logout after the test (default: false).
|
||||
* @param activityRule Optional `TestRule` for managing activities (default: null).
|
||||
* @param setUp A lambda function containing setup logic to be executed before each test (default: empty).
|
||||
* @param afterHilt A lambda function containing setup logic to be executed before each test (default: empty).
|
||||
* @return A new `ProtonRule` instance.
|
||||
*/
|
||||
@SuppressWarnings("LongParameterList")
|
||||
public fun Any.protonRule(
|
||||
vararg annotationTestData: AnnotationTestData<Annotation>,
|
||||
annotationTestData: Set<AnnotationTestData<Annotation>> = emptySet(),
|
||||
envConfig: EnvironmentConfig? = null,
|
||||
userData: TestUserData? = null,
|
||||
loginBefore: Boolean = false,
|
||||
logoutBefore: Boolean = false,
|
||||
logoutAfter: Boolean = false,
|
||||
activityRule: TestRule? = null,
|
||||
setUp: () -> Any = { },
|
||||
afterHilt: (ProtonRule) -> Any = { },
|
||||
beforeHilt: (ProtonRule) -> Any = { },
|
||||
): ProtonRule {
|
||||
val userConfig = ProtonRule.UserConfig(
|
||||
val userConfig = UserConfig(
|
||||
userData = userData,
|
||||
loginBefore = loginBefore,
|
||||
logoutBefore = logoutBefore,
|
||||
logoutAfter = logoutAfter
|
||||
)
|
||||
|
||||
val testConfig = ProtonRule.TestConfig(
|
||||
val testConfig = TestConfig(
|
||||
envConfig = envConfig,
|
||||
annotationTestData = annotationTestData,
|
||||
activityRule = activityRule
|
||||
)
|
||||
|
||||
val hiltConfig = HiltConfig(
|
||||
hiltInstance = this,
|
||||
afterHilt = afterHilt,
|
||||
beforeHilt = beforeHilt
|
||||
)
|
||||
|
||||
return ProtonRule(
|
||||
userConfig = userConfig,
|
||||
testConfig = testConfig,
|
||||
hiltTestInstance = this,
|
||||
setup = setUp
|
||||
hiltConfig = hiltConfig
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -102,14 +111,15 @@ public fun Any.protonRule(
|
|||
*/
|
||||
@SuppressWarnings("LongParameterList")
|
||||
public inline fun <reified A : Activity> Any.protonActivityScenarioRule(
|
||||
vararg annotationTestData: AnnotationTestData<Annotation>,
|
||||
annotationTestData: Set<AnnotationTestData<Annotation>> = emptySet(),
|
||||
envConfig: EnvironmentConfig? = null,
|
||||
userData: TestUserData? = TestUserData.withRandomUsername,
|
||||
loginBefore: Boolean = true,
|
||||
logoutBefore: Boolean = true,
|
||||
logoutAfter: Boolean = true,
|
||||
activityScenarioRule: ActivityScenarioRule<A> = activityScenarioRule(),
|
||||
noinline setUp: () -> Any = { },
|
||||
noinline beforeHilt: (ProtonRule) -> Unit = { },
|
||||
noinline afterHilt: (ProtonRule) -> Unit = { },
|
||||
): ProtonRule = protonRule(
|
||||
annotationTestData = annotationTestData,
|
||||
envConfig = envConfig,
|
||||
|
@ -118,7 +128,8 @@ public inline fun <reified A : Activity> Any.protonActivityScenarioRule(
|
|||
logoutBefore = logoutBefore,
|
||||
logoutAfter = logoutAfter,
|
||||
activityRule = activityScenarioRule,
|
||||
setUp = setUp
|
||||
afterHilt = afterHilt,
|
||||
beforeHilt = beforeHilt
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -136,19 +147,20 @@ public inline fun <reified A : Activity> Any.protonActivityScenarioRule(
|
|||
* @param logoutAfter Whether to perform logout after the test (default: true).
|
||||
* @param composeTestRule A `ComposeTestRule` for the specified component activity type
|
||||
* (default: created using `createAndroidComposeRule()`).
|
||||
* @param setUp A lambda function containing setup logic to be executed before each test (default: empty).
|
||||
* @param afterHilt A lambda function containing setup logic to be executed before each test (default: empty).
|
||||
* @return A new `ProtonRule` instance.
|
||||
*/
|
||||
@SuppressWarnings("LongParameterList")
|
||||
public inline fun <reified A : ComponentActivity> Any.protonAndroidComposeRule(
|
||||
vararg annotationTestData: AnnotationTestData<Annotation>,
|
||||
annotationTestData: Set<AnnotationTestData<Annotation>> = emptySet(),
|
||||
envConfig: EnvironmentConfig? = null,
|
||||
userData: TestUserData? = TestUserData.withRandomUsername,
|
||||
loginBefore: Boolean = true,
|
||||
logoutBefore: Boolean = true,
|
||||
logoutAfter: Boolean = true,
|
||||
composeTestRule: ComposeTestRule = createAndroidComposeRule<A>(),
|
||||
noinline setUp: () -> Any = { },
|
||||
noinline beforeHilt: (ProtonRule) -> Any = { },
|
||||
noinline afterHilt: (ProtonRule) -> Any = { },
|
||||
): ProtonRule = protonRule(
|
||||
annotationTestData = annotationTestData,
|
||||
envConfig = envConfig,
|
||||
|
@ -157,5 +169,6 @@ public inline fun <reified A : ComponentActivity> Any.protonAndroidComposeRule(
|
|||
logoutBefore = logoutBefore,
|
||||
logoutAfter = logoutAfter,
|
||||
activityRule = composeTestRule,
|
||||
setUp = setUp
|
||||
beforeHilt = beforeHilt,
|
||||
afterHilt = afterHilt
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue