From f1cf3a634cc13d873fa2f449f044d3b9fe581ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Artiom=20Ko=C5=A1elev?= Date: Tue, 5 Sep 2023 12:06:50 +0300 Subject: [PATCH] feat: Added configuration module. --- README.md | 5 + configuration/build.gradle.kts | 39 +++++ .../configuration-dagger-content-resolver.api | 24 +++ .../dagger/content-resolver/build.gradle.kts | 44 +++++ .../ContentResolverEnvironmentConfigModule.kt | 40 +++++ .../configuration-dagger-staticdefaults.api | 24 +++ .../dagger/staticdefaults/build.gradle.kts | 44 +++++ .../dagger/StaticEnvironmentConfigModule.kt | 36 ++++ .../domain/api/configuration-domain.api | 68 ++++++++ configuration/domain/build.gradle.kts | 40 +++++ .../configuration/entity/ConfigContract.kt | 30 ++++ .../entity/ConfigFieldProvider.kt | 30 ++++ .../configuration/entity/DefaultConfig.kt | 31 ++++ .../entity/EnvironmentConfiguration.kt | 42 +++++ .../configuration/extension/ConfigContract.kt | 46 +++++ .../core/configuration/ConfigContractTest.kt | 88 ++++++++++ .../configuration/ConfigFieldProviderTest.kt | 63 +++++++ .../EnvironmentConfigurationComparisonTest.kt | 109 ++++++++++++ .../EnvironmentConfigurationTest.kt | 112 ++++++++++++ .../provider/api/configuration-provider.api | 28 +++ configuration/provider/build.gradle.kts | 43 +++++ .../provider/ContentResolverConfigProvider.kt | 50 ++++++ .../provider/StaticConfigProvider.kt | 59 +++++++ .../provider/ConfigProviderTest.kt | 160 ++++++++++++++++++ .../ContentResolverConfigProviderTest.kt | 102 +++++++++++ .../configuration/provider/data/MockConfig.kt | 51 ++++++ plugins/core/src/main/kotlin/modules.kt | 7 + 27 files changed, 1415 insertions(+) create mode 100644 configuration/build.gradle.kts create mode 100644 configuration/dagger/content-resolver/api/configuration-dagger-content-resolver.api create mode 100644 configuration/dagger/content-resolver/build.gradle.kts create mode 100644 configuration/dagger/content-resolver/src/main/kotlin/me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule.kt create mode 100644 configuration/dagger/staticdefaults/api/configuration-dagger-staticdefaults.api create mode 100644 configuration/dagger/staticdefaults/build.gradle.kts create mode 100644 configuration/dagger/staticdefaults/src/main/kotlin/me/proton/core/configuration/dagger/StaticEnvironmentConfigModule.kt create mode 100644 configuration/domain/api/configuration-domain.api create mode 100644 configuration/domain/build.gradle.kts create mode 100644 configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigContract.kt create mode 100644 configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigFieldProvider.kt create mode 100644 configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/DefaultConfig.kt create mode 100644 configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/EnvironmentConfiguration.kt create mode 100644 configuration/domain/src/main/kotlin/me/proton/core/configuration/extension/ConfigContract.kt create mode 100644 configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigContractTest.kt create mode 100644 configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigFieldProviderTest.kt create mode 100644 configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationComparisonTest.kt create mode 100644 configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationTest.kt create mode 100644 configuration/provider/api/configuration-provider.api create mode 100644 configuration/provider/build.gradle.kts create mode 100644 configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/ContentResolverConfigProvider.kt create mode 100644 configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/StaticConfigProvider.kt create mode 100644 configuration/provider/src/test/kotlin/me/proton/configuration/provider/ConfigProviderTest.kt create mode 100644 configuration/provider/src/test/kotlin/me/proton/configuration/provider/ContentResolverConfigProviderTest.kt create mode 100644 configuration/provider/src/test/kotlin/me/proton/configuration/provider/data/MockConfig.kt diff --git a/README.md b/README.md index 2b300607c..08b77cda1 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,11 @@ Core libraries coordinates can be found under [coordinates section](#coordinates | me.proton.core:contact-data | | me.proton.core:contact-domain | | me.proton.core:contact-dagger | +| me.proton.core:configuration | +| me.proton.core:configuration-domain | +| me.proton.core:configuration-provider | +| me.proton.core:configuration-dagger-staticdefaults | +| me.proton.core:configuration-dagger-content-resolver | | me.proton.core:core-platform | | me.proton.core:country | | me.proton.core:country-dagger | diff --git a/configuration/build.gradle.kts b/configuration/build.gradle.kts new file mode 100644 index 000000000..4ccf896ff --- /dev/null +++ b/configuration/build.gradle.kts @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +import studio.forface.easygradle.dsl.* + +plugins { + protonAndroidLibrary +} + +protonCoverage.disabled.set(true) +publishOption.shouldBePublishedAsLib = true + +android { + namespace = "me.proton.core.configuration" +} + +dependencies { + api( + project(Module.configurationProvider), + project(Module.configurationDomain), + ) +} + +dependencyAnalysis.issues { onAny { severity("ignore") } } diff --git a/configuration/dagger/content-resolver/api/configuration-dagger-content-resolver.api b/configuration/dagger/content-resolver/api/configuration-dagger-content-resolver.api new file mode 100644 index 000000000..6caef860b --- /dev/null +++ b/configuration/dagger/content-resolver/api/configuration-dagger-content-resolver.api @@ -0,0 +1,24 @@ +public class hilt_aggregated_deps/_me_proton_core_configuration_dagger_ContentResolverEnvironmentConfigModule { + public fun ()V +} + +public final class me/proton/android/configuration/dagger/contentresolver/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public final class me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule { + public fun ()V + public final fun provideContentResolverEnvironmentConfig (Landroid/content/Context;)Lme/proton/core/configuration/entity/EnvironmentConfiguration; +} + +public final class me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule_ProvideContentResolverEnvironmentConfigFactory : dagger/internal/Factory { + public fun (Lme/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule;Ljavax/inject/Provider;)V + public static fun create (Lme/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule;Ljavax/inject/Provider;)Lme/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule_ProvideContentResolverEnvironmentConfigFactory; + public synthetic fun get ()Ljava/lang/Object; + public fun get ()Lme/proton/core/configuration/entity/EnvironmentConfiguration; + public static fun provideContentResolverEnvironmentConfig (Lme/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule;Landroid/content/Context;)Lme/proton/core/configuration/entity/EnvironmentConfiguration; +} + diff --git a/configuration/dagger/content-resolver/build.gradle.kts b/configuration/dagger/content-resolver/build.gradle.kts new file mode 100644 index 000000000..80e9c154c --- /dev/null +++ b/configuration/dagger/content-resolver/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +import studio.forface.easygradle.dsl.* + +plugins { + protonAndroidLibrary + protonDagger +} + +publishOption.shouldBePublishedAsLib = true +protonCoverage.disabled.set(true) + +android { + namespace = "me.proton.android.configuration.dagger.contentresolver" +} + +dependencies { + implementation( + project(Module.configurationDomain), + project(Module.configurationProvider), + project(Module.kotlinUtil) + ) + + testImplementation( + mockk, + junit + ) +} \ No newline at end of file diff --git a/configuration/dagger/content-resolver/src/main/kotlin/me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule.kt b/configuration/dagger/content-resolver/src/main/kotlin/me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule.kt new file mode 100644 index 000000000..917220ca9 --- /dev/null +++ b/configuration/dagger/content-resolver/src/main/kotlin/me/proton/core/configuration/dagger/ContentResolverEnvironmentConfigModule.kt @@ -0,0 +1,40 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.dagger + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import me.proton.core.configuration.entity.EnvironmentConfiguration +import me.proton.core.configuration.provider.ContentResolverConfigProvider +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +public class ContentResolverEnvironmentConfigModule { + @Provides + @Singleton + public fun provideContentResolverEnvironmentConfig(@ApplicationContext context: Context): EnvironmentConfiguration { + val configFieldProvider = ContentResolverConfigProvider(context) + return EnvironmentConfiguration(configFieldProvider) + } +} diff --git a/configuration/dagger/staticdefaults/api/configuration-dagger-staticdefaults.api b/configuration/dagger/staticdefaults/api/configuration-dagger-staticdefaults.api new file mode 100644 index 000000000..c178a01e5 --- /dev/null +++ b/configuration/dagger/staticdefaults/api/configuration-dagger-staticdefaults.api @@ -0,0 +1,24 @@ +public class hilt_aggregated_deps/_me_proton_core_configuration_dagger_StaticEnvironmentConfigModule { + public fun ()V +} + +public final class me/proton/android/configuration/dagger/staticdefaults/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public final class me/proton/core/configuration/dagger/StaticEnvironmentConfigModule { + public fun ()V + public final fun provideStaticEnvironmentConfiguration ()Lme/proton/core/configuration/entity/EnvironmentConfiguration; +} + +public final class me/proton/core/configuration/dagger/StaticEnvironmentConfigModule_ProvideStaticEnvironmentConfigurationFactory : dagger/internal/Factory { + public fun (Lme/proton/core/configuration/dagger/StaticEnvironmentConfigModule;)V + public static fun create (Lme/proton/core/configuration/dagger/StaticEnvironmentConfigModule;)Lme/proton/core/configuration/dagger/StaticEnvironmentConfigModule_ProvideStaticEnvironmentConfigurationFactory; + public synthetic fun get ()Ljava/lang/Object; + public fun get ()Lme/proton/core/configuration/entity/EnvironmentConfiguration; + public static fun provideStaticEnvironmentConfiguration (Lme/proton/core/configuration/dagger/StaticEnvironmentConfigModule;)Lme/proton/core/configuration/entity/EnvironmentConfiguration; +} + diff --git a/configuration/dagger/staticdefaults/build.gradle.kts b/configuration/dagger/staticdefaults/build.gradle.kts new file mode 100644 index 000000000..ff077649c --- /dev/null +++ b/configuration/dagger/staticdefaults/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +import studio.forface.easygradle.dsl.* + +plugins { + protonAndroidLibrary + protonDagger +} + +publishOption.shouldBePublishedAsLib = true +protonCoverage.disabled.set(true) + +android { + namespace = "me.proton.android.configuration.dagger.staticdefaults" +} + +dependencies { + implementation( + project(Module.configurationDomain), + project(Module.configurationProvider), + project(Module.kotlinUtil) + ) + + testImplementation( + mockk, + junit + ) +} diff --git a/configuration/dagger/staticdefaults/src/main/kotlin/me/proton/core/configuration/dagger/StaticEnvironmentConfigModule.kt b/configuration/dagger/staticdefaults/src/main/kotlin/me/proton/core/configuration/dagger/StaticEnvironmentConfigModule.kt new file mode 100644 index 000000000..19b4f435a --- /dev/null +++ b/configuration/dagger/staticdefaults/src/main/kotlin/me/proton/core/configuration/dagger/StaticEnvironmentConfigModule.kt @@ -0,0 +1,36 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.dagger + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import me.proton.core.configuration.entity.EnvironmentConfiguration +import me.proton.core.configuration.provider.StaticConfigFieldProvider +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +public class StaticEnvironmentConfigModule { + @Provides + @Singleton + public fun provideStaticEnvironmentConfiguration(): EnvironmentConfiguration = + EnvironmentConfiguration(StaticConfigFieldProvider()) +} diff --git a/configuration/domain/api/configuration-domain.api b/configuration/domain/api/configuration-domain.api new file mode 100644 index 000000000..41e2ab84e --- /dev/null +++ b/configuration/domain/api/configuration-domain.api @@ -0,0 +1,68 @@ +public final class me/proton/core/configuration/domain/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public abstract interface class me/proton/core/configuration/entity/ConfigContract { + public abstract fun getApiHost ()Ljava/lang/String; + public abstract fun getApiPrefix ()Ljava/lang/String; + public abstract fun getBaseUrl ()Ljava/lang/String; + public abstract fun getHost ()Ljava/lang/String; + public abstract fun getHv3Host ()Ljava/lang/String; + public abstract fun getHv3Url ()Ljava/lang/String; + public abstract fun getProxyToken ()Ljava/lang/String; + public abstract fun getUseDefaultPins ()Ljava/lang/Boolean; +} + +public abstract interface class me/proton/core/configuration/entity/ConfigFieldProvider { + public abstract fun booleanProvider (Ljava/lang/String;)Ljava/lang/Boolean; + public abstract fun getConfigData ()Ljava/util/Map; + public abstract fun stringProvider (Ljava/lang/String;)Ljava/lang/String; +} + +public final class me/proton/core/configuration/entity/ConfigFieldProvider$DefaultImpls { + public static fun booleanProvider (Lme/proton/core/configuration/entity/ConfigFieldProvider;Ljava/lang/String;)Ljava/lang/Boolean; + public static fun stringProvider (Lme/proton/core/configuration/entity/ConfigFieldProvider;Ljava/lang/String;)Ljava/lang/String; +} + +public final class me/proton/core/configuration/entity/DefaultConfig : me/proton/core/configuration/entity/ConfigContract { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;)V + public fun getApiHost ()Ljava/lang/String; + public fun getApiPrefix ()Ljava/lang/String; + public fun getBaseUrl ()Ljava/lang/String; + 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 fun getUseDefaultPins ()Ljava/lang/Boolean; +} + +public final class me/proton/core/configuration/entity/EnvironmentConfiguration : me/proton/core/configuration/entity/ConfigContract { + public fun (Lme/proton/core/configuration/entity/ConfigFieldProvider;)V + public final fun component1 ()Lme/proton/core/configuration/entity/ConfigFieldProvider; + public final fun copy (Lme/proton/core/configuration/entity/ConfigFieldProvider;)Lme/proton/core/configuration/entity/EnvironmentConfiguration; + public static synthetic fun copy$default (Lme/proton/core/configuration/entity/EnvironmentConfiguration;Lme/proton/core/configuration/entity/ConfigFieldProvider;ILjava/lang/Object;)Lme/proton/core/configuration/entity/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/ConfigFieldProvider; + 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 fun getUseDefaultPins ()Ljava/lang/Boolean; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class me/proton/core/configuration/extension/ConfigContractKt { + public static final fun getApiPins (Lme/proton/core/configuration/entity/ConfigContract;)Ljava/util/List; + public static final fun getCertificatePins (Lme/proton/core/configuration/entity/ConfigContract;)[Ljava/lang/String; + public static final fun getDohProvidersUrls ()[Ljava/lang/String; + public static final fun getTestTag ()Ljava/lang/String; + public static final fun mergeWith (Lme/proton/core/configuration/entity/ConfigContract;Lme/proton/core/configuration/entity/ConfigContract;)Lme/proton/core/configuration/entity/ConfigContract; +} + diff --git a/configuration/domain/build.gradle.kts b/configuration/domain/build.gradle.kts new file mode 100644 index 000000000..ce54ae393 --- /dev/null +++ b/configuration/domain/build.gradle.kts @@ -0,0 +1,40 @@ +import studio.forface.easygradle.dsl.* + +/* + * 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 . + */ + +plugins { + protonAndroidLibrary + protonDagger +} + +publishOption.shouldBePublishedAsLib = true + +android { + namespace = "me.proton.core.configuration.domain" +} + +protonCoverage { + minBranchCoveragePercentage.set(75) +} + +dependencies { + implementation(project(Module.networkData)) + + testImplementation(junit, mockk) +} diff --git a/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigContract.kt b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigContract.kt new file mode 100644 index 000000000..af5a9540b --- /dev/null +++ b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigContract.kt @@ -0,0 +1,30 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.entity + +public interface ConfigContract { + public val host: String? + public val proxyToken: String? + public val apiPrefix: String? + public val baseUrl: String? + public val apiHost: String? + public val hv3Host: String? + public val hv3Url: String? + public val useDefaultPins: Boolean? +} diff --git a/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigFieldProvider.kt b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigFieldProvider.kt new file mode 100644 index 000000000..7d3e34328 --- /dev/null +++ b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/ConfigFieldProvider.kt @@ -0,0 +1,30 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.entity + +public interface ConfigFieldProvider { + + public val configData: Map + + public fun stringProvider(fieldName: String): String? = + configData[fieldName].takeIf { configData.contains(fieldName) } as? String + + public fun booleanProvider(fieldName: String): Boolean? = + configData[fieldName].takeIf { configData.contains(fieldName) } as? Boolean +} diff --git a/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/DefaultConfig.kt b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/DefaultConfig.kt new file mode 100644 index 000000000..5e823d836 --- /dev/null +++ b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/DefaultConfig.kt @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.entity + +@Suppress("LongParameterList") +public class DefaultConfig( + override val host: String?, + override val proxyToken: String?, + override val apiPrefix: String?, + override val baseUrl: String?, + override val apiHost: String?, + override val hv3Host: String?, + override val hv3Url: String?, + override val useDefaultPins: Boolean? +) : ConfigContract diff --git a/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/EnvironmentConfiguration.kt b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/EnvironmentConfiguration.kt new file mode 100644 index 000000000..3c6cd0eff --- /dev/null +++ b/configuration/domain/src/main/kotlin/me/proton/core/configuration/entity/EnvironmentConfiguration.kt @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.entity + +import kotlin.reflect.KProperty + +public data class EnvironmentConfiguration( + val configFieldProvider: ConfigFieldProvider +) : ConfigContract { + override val host: String? by provide(this::host) + override val proxyToken: String? by provide(this::proxyToken) + override val apiPrefix: String? by provide(this::apiPrefix) + override val baseUrl: String? by provide(this::baseUrl) + override val apiHost: String? by provide(this::apiHost) + override val hv3Host: String? by provide(this::hv3Host) + override val hv3Url: String? by provide(this::hv3Url) + override val useDefaultPins: Boolean? by provide(this::useDefaultPins) + + public inline fun provide(property: KProperty<*>): Lazy = lazy { + when (T::class) { + String::class -> configFieldProvider.stringProvider(property.name) as? T + Boolean::class -> configFieldProvider.booleanProvider(property.name) as? T + else -> throw IllegalArgumentException("Unsupported Environment Configuration type") + } + } +} diff --git a/configuration/domain/src/main/kotlin/me/proton/core/configuration/extension/ConfigContract.kt b/configuration/domain/src/main/kotlin/me/proton/core/configuration/extension/ConfigContract.kt new file mode 100644 index 000000000..fe3548c08 --- /dev/null +++ b/configuration/domain/src/main/kotlin/me/proton/core/configuration/extension/ConfigContract.kt @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.extension + +import me.proton.core.configuration.entity.ConfigContract +import me.proton.core.configuration.entity.DefaultConfig +import me.proton.core.network.data.di.Constants +import me.proton.core.network.data.di.Constants.DEFAULT_SPKI_PINS + +public val ConfigContract.certificatePins: Array + get() = if (useDefaultPins == true) DEFAULT_SPKI_PINS else emptyArray() + +public val ConfigContract.apiPins: List + get() = + if (useDefaultPins == true) Constants.ALTERNATIVE_API_SPKI_PINS else emptyList() + +public val dohProvidersUrls: Array get() = Constants.DOH_PROVIDERS_URLS + +public val testTag: String get() = "me.proton.core.configuration" + +public fun ConfigContract.mergeWith(other: ConfigContract): ConfigContract = DefaultConfig( + other.host ?: host, + other.proxyToken ?: proxyToken, + other.apiPrefix ?: apiPrefix, + other.baseUrl ?: baseUrl, + other.apiHost ?: apiHost, + other.hv3Host ?: hv3Host, + other.hv3Url ?: hv3Url, + other.useDefaultPins ?: useDefaultPins +) diff --git a/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigContractTest.kt b/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigContractTest.kt new file mode 100644 index 000000000..c6cfd6b00 --- /dev/null +++ b/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigContractTest.kt @@ -0,0 +1,88 @@ +/* + * 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 . + */ + +package me.proton.core.configuration + +import me.proton.core.configuration.entity.DefaultConfig +import me.proton.core.configuration.extension.apiPins +import me.proton.core.configuration.extension.certificatePins +import me.proton.core.configuration.extension.mergeWith +import me.proton.core.network.data.di.Constants +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConfigContractTest { + + private val baseConfig = DefaultConfig( + host = "baseHost", + proxyToken = "baseProxyToken", + apiPrefix = "baseApiPrefix", + baseUrl = "baseBaseUrl", + apiHost = "baseApiHost", + hv3Host = "baseHv3Host", + hv3Url = "baseHv3Url", + useDefaultPins = true + ) + + @Test + fun `should merge two ConfigContracts correctly`() { + + val overrideConfig = DefaultConfig( + host = "overrideHost", + proxyToken = null, + apiPrefix = "overrideApiPrefix", + baseUrl = null, + apiHost = "overrideApiHost", + hv3Host = null, + hv3Url = "overrideHv3Url", + useDefaultPins = false + ) + + val mergedConfig = baseConfig.mergeWith(overrideConfig) + + assertEquals("overrideHost", mergedConfig.host) + assertEquals("baseProxyToken", mergedConfig.proxyToken) + assertEquals("overrideApiPrefix", mergedConfig.apiPrefix) + assertEquals("baseBaseUrl", mergedConfig.baseUrl) + assertEquals("overrideApiHost", mergedConfig.apiHost) + assertEquals("baseHv3Host", mergedConfig.hv3Host) + assertEquals("overrideHv3Url", mergedConfig.hv3Url) + assertEquals(false, mergedConfig.useDefaultPins) + } + + @Test + fun `should return correct values for properties`() { + val configWithoutDefaultPins = DefaultConfig( + host = "testHost", + proxyToken = "testProxyToken", + apiPrefix = "testApiPrefix", + baseUrl = "testBaseUrl", + apiHost = "testApiHost", + hv3Host = "testHv3Host", + hv3Url = "testHv3Url", + useDefaultPins = false + ) + + assertArrayEquals(Constants.DEFAULT_SPKI_PINS, baseConfig.certificatePins) + assertArrayEquals(emptyArray(), configWithoutDefaultPins.certificatePins) + + assertEquals(Constants.ALTERNATIVE_API_SPKI_PINS, baseConfig.apiPins) + assertEquals(emptyList(), configWithoutDefaultPins.apiPins) + } +} diff --git a/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigFieldProviderTest.kt b/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigFieldProviderTest.kt new file mode 100644 index 000000000..6f4c8e949 --- /dev/null +++ b/configuration/domain/src/test/kotlin/me/proton/core/configuration/ConfigFieldProviderTest.kt @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +package me.proton.core.configuration + +import me.proton.core.configuration.entity.ConfigFieldProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class ConfigFieldProviderTest { + + private lateinit var configFieldProvider: ConfigFieldProvider + + val mockConfigData: Map = mapOf( + "stringField" to "stringValue", + "booleanField" to true + ) + + @Before + fun setUp() { + configFieldProvider = object : ConfigFieldProvider { + override val configData: Map = mockConfigData + } + } + + @Test + fun `test stringProvider with existing field`() { + assertEquals("stringValue", configFieldProvider.stringProvider("stringField")) + } + + @Test + fun `test stringProvider with non-existing field`() { + assertNull(configFieldProvider.stringProvider("nonExistentField")) + } + + @Test + fun `test booleanProvider with existing field`() { + assertTrue(configFieldProvider.booleanProvider("booleanField")!!) + } + + @Test + fun `test booleanProvider with non-existing field`() { + assertNull(configFieldProvider.booleanProvider("nonExistentField")) + } +} diff --git a/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationComparisonTest.kt b/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationComparisonTest.kt new file mode 100644 index 000000000..f6f963767 --- /dev/null +++ b/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationComparisonTest.kt @@ -0,0 +1,109 @@ +/* + * 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 . + */ + +package me.proton.core.configuration + +import io.mockk.every +import io.mockk.mockk +import me.proton.core.configuration.entity.ConfigContract +import me.proton.core.configuration.entity.ConfigFieldProvider +import me.proton.core.configuration.entity.DefaultConfig +import me.proton.core.configuration.entity.EnvironmentConfiguration +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import kotlin.reflect.KProperty + +class EnvironmentConfigurationComparisonTest { + + private lateinit var environmentConfiguration: EnvironmentConfiguration + private lateinit var defaultEnvironmentConfiguration: DefaultConfig + + val mockConfigData: Map = mapOf( + "host" to "testHost", + "proxyToken" to "testProxyToken", + "apiPrefix" to "testApiPrefix", + "baseUrl" to "testBaseUrl", + "apiHost" to null, + "hv3Host" to "testHv3Host", + "hv3Url" to "testHv3Url", + "useDefaultPins" to true + ) + + @Before + fun setUp() { + defaultEnvironmentConfiguration = DefaultConfig( + host = "testHost", + proxyToken = "testProxyToken", + apiPrefix = "testApiPrefix", + baseUrl = "testBaseUrl", + apiHost = null, + hv3Host = "testHv3Host", + hv3Url = "testHv3Url", + useDefaultPins = true + ) + + val configFieldProvider = object : ConfigFieldProvider { + override val configData: Map = mockConfigData + } + + environmentConfiguration = EnvironmentConfiguration(configFieldProvider) + } + + @Test + fun `should match expected and actual field values`() { + ConfigContract::class.java.declaredFields.forEach { property -> + val expectedValue = property.get(defaultEnvironmentConfiguration) + val actualValue = property.get(environmentConfiguration) + assertEquals(expectedValue, actualValue) + } + } + + @Test + fun `should return null for non-existent string field`() { + assertNull(environmentConfiguration.configFieldProvider.stringProvider("nonExistentField")) + } + + @Test + fun `should return null for non-existent boolean field`() { + assertNull(environmentConfiguration.configFieldProvider.booleanProvider("nonExistentField")) + } + + @Test + fun `should return correct string value for existing field`() { + assertEquals("testHost", environmentConfiguration.configFieldProvider.stringProvider("host")) + } + + @Test + fun `should return correct boolean value for existing field`() { + assertEquals(true, environmentConfiguration.configFieldProvider.booleanProvider("useDefaultPins")) + } + + + @Test + fun `should throw exception for unsupported type in provide function`() { + val mockProperty = mockk>() + every { mockProperty.name } returns "intField" + + assertThrows(IllegalArgumentException::class.java) { + environmentConfiguration.provide(mockProperty).value + } + } +} diff --git a/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationTest.kt b/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationTest.kt new file mode 100644 index 000000000..7144a0de5 --- /dev/null +++ b/configuration/domain/src/test/kotlin/me/proton/core/configuration/EnvironmentConfigurationTest.kt @@ -0,0 +1,112 @@ +/* + * 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 . + */ + +package me.proton.core.configuration + +import io.mockk.every +import io.mockk.mockk +import me.proton.core.configuration.entity.ConfigFieldProvider +import me.proton.core.configuration.entity.DefaultConfig +import me.proton.core.configuration.entity.EnvironmentConfiguration +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import kotlin.reflect.KProperty + +class EnvironmentConfigurationTest { + + private lateinit var configFieldProvider: ConfigFieldProvider + + @Before + fun setUp() { + configFieldProvider = mockk() + } + + @Test + fun `should provide String value when property type is String`() { + val mockProperty = mockk>() + every { mockProperty.name } returns "host" + every { configFieldProvider.stringProvider("host") } returns "testHost" + + val result: Lazy = provide(mockProperty) + assertEquals("testHost", result.value) + } + + @Test + fun `should provide Boolean value when property type is Boolean`() { + val mockProperty = mockk>() + every { mockProperty.name } returns "useDefaultPins" + every { configFieldProvider.booleanProvider("useDefaultPins") } returns true + + val result: Lazy = provide(mockProperty) + assertEquals(true, result.value) + } + + @Test + fun `should throw exception for unsupported type`() { + val mockProperty = mockk>() + every { mockProperty.name } returns "unsupportedField" + + assertThrows(IllegalArgumentException::class.java) { + val result: Lazy = provide(mockProperty) + result.value + } + } + + @Test + fun `default configuration class`() { + val expectedConfig = DefaultConfig( + "host", + null, + "test'///12", + null, + "apiHost", + null, + null, + false + ) + + val fieldProvider = object : ConfigFieldProvider { + override val configData = DefaultConfig::class.java.declaredFields.associate { + it.isAccessible = true + it.name to it.get(expectedConfig) + } + } + + val envConfig = EnvironmentConfiguration(fieldProvider) + + assertEquals(expectedConfig.host, envConfig.host) + assertEquals(expectedConfig.proxyToken, envConfig.proxyToken) + assertEquals(expectedConfig.apiPrefix, envConfig.apiPrefix) + assertEquals(expectedConfig.baseUrl, envConfig.baseUrl) + assertEquals(expectedConfig.apiHost, envConfig.apiHost) + assertEquals(expectedConfig.hv3Host, envConfig.hv3Host) + assertEquals(expectedConfig.hv3Host, envConfig.hv3Host) + assertEquals(expectedConfig.hv3Url, envConfig.hv3Url) + assertEquals(expectedConfig.useDefaultPins, envConfig.useDefaultPins) + } + + private inline fun provide(property: KProperty<*>): Lazy = lazy { + when (T::class) { + String::class -> configFieldProvider.stringProvider(property.name) as? T + Boolean::class -> configFieldProvider.booleanProvider(property.name) as? T + else -> throw IllegalArgumentException("Unsupported Environment Configuration type") + } + } +} diff --git a/configuration/provider/api/configuration-provider.api b/configuration/provider/api/configuration-provider.api new file mode 100644 index 000000000..692d21229 --- /dev/null +++ b/configuration/provider/api/configuration-provider.api @@ -0,0 +1,28 @@ +public final class me/proton/core/configuration/provider/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public class me/proton/core/configuration/provider/ContentResolverConfigProvider : me/proton/core/configuration/entity/ConfigFieldProvider { + public fun (Landroid/content/Context;Lme/proton/core/configuration/entity/ConfigFieldProvider;)V + public synthetic fun (Landroid/content/Context;Lme/proton/core/configuration/entity/ConfigFieldProvider;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun booleanProvider (Ljava/lang/String;)Ljava/lang/Boolean; + public fun getConfigData ()Ljava/util/Map; + public fun stringProvider (Ljava/lang/String;)Ljava/lang/String; +} + +public final class me/proton/core/configuration/provider/StaticConfigFieldProvider : me/proton/core/configuration/entity/ConfigFieldProvider { + public fun ()V + public fun (Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun booleanProvider (Ljava/lang/String;)Ljava/lang/Boolean; + public fun getConfigData ()Ljava/util/Map; + public fun stringProvider (Ljava/lang/String;)Ljava/lang/String; +} + +public final class me/proton/core/configuration/provider/StaticConfigProviderKt { + public static final field DEFAULTS_CLASS Ljava/lang/String; +} + diff --git a/configuration/provider/build.gradle.kts b/configuration/provider/build.gradle.kts new file mode 100644 index 000000000..eadb39de5 --- /dev/null +++ b/configuration/provider/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +import studio.forface.easygradle.dsl.* + +plugins { + protonAndroidLibrary + protonDagger +} + +publishOption.shouldBePublishedAsLib = true + +android { + namespace = "me.proton.core.configuration.provider" +} + +protonCoverage { + minBranchCoveragePercentage.set(80) +} + +dependencies { + implementation( + project(Module.networkData), + project(Module.configurationDomain) + ) + + testImplementation(junit, mockk) +} diff --git a/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/ContentResolverConfigProvider.kt b/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/ContentResolverConfigProvider.kt new file mode 100644 index 000000000..465b85559 --- /dev/null +++ b/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/ContentResolverConfigProvider.kt @@ -0,0 +1,50 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.provider + +import android.content.Context +import android.database.Cursor +import android.net.Uri +import me.proton.core.configuration.entity.ConfigFieldProvider + +public open class ContentResolverConfigProvider( + context: Context, + private val defaultConfigProvider: ConfigFieldProvider = StaticConfigFieldProvider() +) : ConfigFieldProvider { + + public override val configData: Map by lazy { + fetchConfigDataFromContentResolver(context) ?: defaultConfigProvider.configData + } + + private fun fetchConfigDataFromContentResolver(context: Context): Map? = + context.contentResolver.query(CONTENT_URI, null, null, null, null)?.use { cursor -> + cursor.columnNames.associateWith { columnName -> + cursor.retrieveValue(columnName) + } + } + + private fun Cursor.retrieveValue(columnName: String): Any? { + val columnIndex = getColumnIndex(columnName) + return if (columnIndex != -1 && moveToFirst()) getString(columnIndex) else null + } + + private companion object { + val CONTENT_URI: Uri = Uri.parse("content://me.proton.android.configurator/config") + } +} diff --git a/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/StaticConfigProvider.kt b/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/StaticConfigProvider.kt new file mode 100644 index 000000000..f28c67698 --- /dev/null +++ b/configuration/provider/src/main/kotlin/me/proton/core/configuration/provider/StaticConfigProvider.kt @@ -0,0 +1,59 @@ +/* + * 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 . + */ + +package me.proton.core.configuration.provider + +import me.proton.core.configuration.entity.ConfigFieldProvider +import me.proton.core.configuration.extension.testTag +import me.proton.core.util.kotlin.CoreLogger + +public const val DEFAULTS_CLASS: String = "me.proton.core.configuration.EnvironmentConfigurationDefaults" + +public class StaticConfigFieldProvider(className: String = DEFAULTS_CLASS) : ConfigFieldProvider { + + override val configData: Map + + init { + val classNotFoundErrorMessage = + "Class not found: $className. Make sure environment configuration gradle plugin is enabled!" + + configData = try { + val defaultsClass = Class.forName(className) + val instance = defaultsClass.newInstance() + + defaultsClass + .declaredFields + .associate { property -> + property.isAccessible = true + val propertyValue = property.get(instance) + + require(propertyValue.isValidConfigValue()) { + "Unexpected value type for property: ${property.name}. " + + "Expected String, Boolean, or null. Found ${propertyValue?.javaClass?.name}." + } + + property.name to propertyValue + } + } catch (e: ClassNotFoundException) { + throw IllegalStateException(classNotFoundErrorMessage, e) + } + } + + private fun Any?.isValidConfigValue() = + this is String || this is Boolean || this == null +} diff --git a/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ConfigProviderTest.kt b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ConfigProviderTest.kt new file mode 100644 index 000000000..5a5c41abd --- /dev/null +++ b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ConfigProviderTest.kt @@ -0,0 +1,160 @@ +/* + * 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 . + */ + +package me.proton.configuration.provider + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import me.proton.configuration.provider.data.EmptyMockConfig +import me.proton.configuration.provider.data.MockConfigWithNullValues +import me.proton.configuration.provider.data.MockConfigWithOtherTypes +import me.proton.configuration.provider.data.MockConfigWithUnexpectedValueType +import me.proton.configuration.provider.data.MockEnvironmentConfig +import me.proton.core.configuration.entity.ConfigContract +import me.proton.core.configuration.entity.ConfigFieldProvider +import me.proton.core.configuration.entity.EnvironmentConfiguration +import me.proton.core.configuration.provider.ContentResolverConfigProvider +import me.proton.core.configuration.provider.StaticConfigFieldProvider +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Assume.assumeTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ConfigProviderTest( + private val provider: ConfigFieldProvider +) { + @Test + fun testConfigDataExtractionFromMockClass() { + + assumeTrue(provider is StaticConfigFieldProvider) + + val kClass = Class.forName(MockEnvironmentConfig::class.java.name) + val instance = kClass.newInstance() + + val expectedData = kClass.declaredFields.associate { + it.isAccessible = true + it.name to it.get(instance) + } + + assertEquals(expectedData, provider.configData) + } + + @Test + fun testEnvironmentConfigurationWithStaticProvider() { + assumeTrue(provider is StaticConfigFieldProvider) + + val expected = MockEnvironmentConfig() as ConfigContract + val actual = EnvironmentConfiguration(provider) as ConfigContract + + assertEquals(expected.host, actual.host) + assertEquals(expected.proxyToken, actual.proxyToken) + assertEquals(expected.apiPrefix, actual.apiPrefix) + assertEquals(expected.baseUrl, actual.baseUrl) + assertEquals(expected.apiHost, actual.apiHost) + assertEquals(expected.hv3Host, actual.hv3Host) + assertEquals(expected.hv3Host, actual.hv3Host) + assertEquals(expected.hv3Url, actual.hv3Url) + assertEquals(expected.useDefaultPins, actual.useDefaultPins) + } + + @Test + fun testUnexpectedValueType() { + assertThrows(IllegalArgumentException::class.java) { + StaticConfigFieldProvider(MockConfigWithUnexpectedValueType::class.java.name) + } + } + + @Test + fun testClassNotFound() { + assertThrows(IllegalStateException::class.java) { + StaticConfigFieldProvider("InvalidClassName") + } + } + + @Test + fun testNullValues() { + val providerWithNullValues = + StaticConfigFieldProvider(MockConfigWithNullValues::class.java.name) + val configData = providerWithNullValues.configData + + assertTrue(configData["nullableString"] == null) + } + + @Test + fun testEmptyClass() { + val providerWithEmptyClass = + StaticConfigFieldProvider(EmptyMockConfig::class.java.name) + val configData = providerWithEmptyClass.configData + + assertTrue(configData.isEmpty()) + } + + @Test + fun testClassWithOtherTypes() { + assertThrows(IllegalArgumentException::class.java) { + StaticConfigFieldProvider(MockConfigWithOtherTypes::class.java.name) + } + } + + companion object { + @SuppressLint("StaticFieldLeak") + private val mockContext = mockk() + private val mockContentResolver = mockk() + private val mockUri = mockk() + + init { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockUri + + every { mockContext.contentResolver } returns mockContentResolver + + val mockCursor = mockk() + every { mockCursor.getColumnIndex(any()) } returns 0 + every { mockCursor.moveToFirst() } returns true + every { mockCursor.getString(any()) } returns "mockValue" + every { mockContentResolver.query(any(), any(), any(), any(), any()) } returns mockCursor + } + + private val staticConfig = + StaticConfigFieldProvider(MockEnvironmentConfig::class.java.name) + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): Collection> { + return listOf( + arrayOf(staticConfig), + arrayOf( + ContentResolverConfigProvider( + mockContext, + defaultConfigProvider = staticConfig + ) + ) + ) + } + } +} diff --git a/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ContentResolverConfigProviderTest.kt b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ContentResolverConfigProviderTest.kt new file mode 100644 index 000000000..891e5a519 --- /dev/null +++ b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/ContentResolverConfigProviderTest.kt @@ -0,0 +1,102 @@ +/* + * 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 . + */ + +package me.proton.configuration.provider + +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor +import android.net.Uri +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import me.proton.core.configuration.entity.ConfigFieldProvider +import me.proton.core.configuration.provider.ContentResolverConfigProvider +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class ContentResolverConfigProviderTest { + + private val mockContext: Context = mockk(relaxed = true) + + private val mockContentResolver: ContentResolver = mockk(relaxed = true) + + private val mockCursor: Cursor = mockk(relaxed = true) + + private val defaultConfigImpl: ConfigFieldProvider = mockk(relaxed = true) + + private val mockUri = mockk() + + @Before + fun setup() { + mockkStatic(Uri::class) + every { Uri.parse(any()) } returns mockUri + every { mockContext.contentResolver } returns mockContentResolver + } + + @Test + fun testFallBackOnNullContentResolver() { + every { mockContentResolver.query(any(), any(), any(), any(), any()) } returns null + + val provider = ContentResolverConfigProvider( + mockContext, + defaultConfigImpl + ) + + assertEquals(defaultConfigImpl.configData, provider.configData) + } + + @Test + fun testFallBackOnNullCursor() { + every { mockContentResolver.query(any(), any(), any(), any(), any()) } returns mockCursor + every { mockCursor.columnNames } returns emptyArray() + + val provider = ContentResolverConfigProvider( + mockContext, + defaultConfigImpl + ) + + assertEquals(defaultConfigImpl.configData, provider.configData) + } + + @Test + fun testValuesFromContentResolverAreUsed() { + val mockData = mapOf( + "host" to "testHost", + "useDefaultPins" to true.toString(), + "apiPrefix" to null.toString() + ) + + every { mockContentResolver.query(any(), any(), any(), any(), any()) } returns mockCursor + every { mockCursor.columnNames } returns mockData.keys.toTypedArray() + every { mockCursor.moveToFirst() } returns true + + mockData.entries.forEachIndexed { index, entry -> + every { mockCursor.getColumnIndex(entry.key) } returns index + every { mockCursor.getString(index) } returns entry.value + } + + val provider = ContentResolverConfigProvider( + mockContext, + defaultConfigImpl + ) + + assertEquals(mockData, provider.configData) + } +} diff --git a/configuration/provider/src/test/kotlin/me/proton/configuration/provider/data/MockConfig.kt b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/data/MockConfig.kt new file mode 100644 index 000000000..0cee47ef5 --- /dev/null +++ b/configuration/provider/src/test/kotlin/me/proton/configuration/provider/data/MockConfig.kt @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +package me.proton.configuration.provider.data + +import me.proton.core.configuration.entity.ConfigContract + +class MockConfigWithNullValues { + val nullableString: String? = null +} + +class EmptyMockConfig + +@Suppress("unused") +class MockConfigWithNonPublicProps { + private val privateString: String = "private" +} + +class MockConfigWithOtherTypes { + val integerProp: Int = 123 +} + +class MockConfigWithUnexpectedValueType { + val host: Int = 123 +} + +class MockEnvironmentConfig : ConfigContract { + override val host: String = "host" + override val proxyToken: String = "proxyToken" + override val apiPrefix: String = "apiPrefix" + override val baseUrl: String? = null + override val apiHost: String = "apiHost" + override val hv3Host: String = "hv3Host" + override val hv3Url: String = "hv3Url" + override val useDefaultPins: Boolean = false +} diff --git a/plugins/core/src/main/kotlin/modules.kt b/plugins/core/src/main/kotlin/modules.kt index 80b6692f7..9aab674e6 100644 --- a/plugins/core/src/main/kotlin/modules.kt +++ b/plugins/core/src/main/kotlin/modules.kt @@ -132,6 +132,13 @@ public object Module { public const val labelData: String = "$label:label-data" public const val labelDagger: String = "$label:label-dagger" + // Configuration + public const val configuration: String = ":configuration" + public const val configurationDomain: String = "$configuration:configuration-domain" + public const val configurationProvider: String = "$configuration:configuration-provider" + public const val configurationDaggerStatic: String = "$configuration:configuration-dagger-staticdefaults" + public const val configurationDaggerContentResolver: String = "$configuration:configuration-dagger-content-resolver" + // Contact public const val contact: String = ":contact" public const val contactDomain: String = "$contact:contact-domain"