From bc727821fbd2afbbb89b0daadddc632afc0cf179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Artiom=20Ko=C5=A1elev?= Date: Fri, 8 Mar 2024 11:02:47 +0200 Subject: [PATCH] feat: Added test rules package. --- README.md | 1 + configuration/data/build.gradle.kts | 6 +- plugins/core/src/main/kotlin/modules.kt | 1 + test/rule/README.md | 202 ++++++++++++++++++ test/rule/api/test-rule.api | 202 ++++++++++++++++++ test/rule/build.gradle.kts | 53 +++++ .../core/test/rule/AuthenticationRule.kt | 71 ++++++ .../core/test/rule/EnvironmentConfigRule.kt | 58 +++++ .../me/proton/core/test/rule/ProtonRule.kt | 124 +++++++++++ .../core/test/rule/QuarkTestDataRule.kt | 124 +++++++++++ .../rule/annotation/AnnotationTestData.kt | 89 ++++++++ .../test/rule/annotation/EnvironmentConfig.kt | 39 ++++ .../rule/annotation/TestPaymentMethods.kt | 51 +++++ .../rule/annotation/TestSubscriptionData.kt | 42 ++++ .../core/test/rule/annotation/TestUserData.kt | 77 +++++++ .../rule/di/TestEnvironmentConfigModule.kt | 69 ++++++ .../core/test/rule/extension/QuarkCommand.kt | 101 +++++++++ .../core/test/rule/extension/TestRule.kt | 161 ++++++++++++++ 18 files changed, 1466 insertions(+), 5 deletions(-) create mode 100644 test/rule/README.md create mode 100644 test/rule/api/test-rule.api create mode 100644 test/rule/build.gradle.kts create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/AuthenticationRule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/EnvironmentConfigRule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/ProtonRule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/AnnotationTestData.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/EnvironmentConfig.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestPaymentMethods.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestSubscriptionData.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestUserData.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/di/TestEnvironmentConfigModule.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/extension/QuarkCommand.kt create mode 100644 test/rule/src/main/kotlin/me/proton/core/test/rule/extension/TestRule.kt diff --git a/README.md b/README.md index cb331c926..d73c1c442 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ Core libraries coordinates can be found under [coordinates section](#coordinates | me.proton.core:test-android-instrumented | | me.proton.core:test-kotlin | | me.proton.core:test-quark | +| me.proton.core:test-rule | | me.proton.core:user | | me.proton.core:user-dagger | | me.proton.core:user-data | diff --git a/configuration/data/build.gradle.kts b/configuration/data/build.gradle.kts index 2ed389028..b15c1920d 100644 --- a/configuration/data/build.gradle.kts +++ b/configuration/data/build.gradle.kts @@ -35,11 +35,7 @@ protonCoverage { } dependencies { - implementation( - project(Module.networkData), - datastore, - datastorePreferences, - ) + implementation(project(Module.networkData)) testImplementation(junit, mockk) } diff --git a/plugins/core/src/main/kotlin/modules.kt b/plugins/core/src/main/kotlin/modules.kt index 3259e898c..d663f6f23 100644 --- a/plugins/core/src/main/kotlin/modules.kt +++ b/plugins/core/src/main/kotlin/modules.kt @@ -38,6 +38,7 @@ public object Module { public const val androidTest: String = "$test:test-android" public const val androidInstrumentedTest: String = "$androidTest:test-android-instrumented" public const val quark: String = "$test:test-quark" + public const val testRule: String = "$test:test-rule" // endregion // region Shared diff --git a/test/rule/README.md b/test/rule/README.md new file mode 100644 index 000000000..674c439d0 --- /dev/null +++ b/test/rule/README.md @@ -0,0 +1,202 @@ +## Proton Rule: Streamlined Test Setup for Android + +**Proton Rule** is an Android Kotlin library designed to simplify and organize test setup in your Android tests, focusing on common use cases for activity and Compose UI testing. + +**Key Features:** + +* **Reduced Boilerplate:** Streamline common tasks like activity/Compose test setup, test data injection, and environment configuration. +* **Improved Organization:** Centralize configuration for cleaner and more maintainable tests. +* **Customization:** Define custom annotations and rules for specific test requirements. + +**Basic Usage:** + +The `protonRule` function provides a central entry point for common test setups: + +```kotlin +@HiltAndroidTest +class TestClass { + + private val testSubscriptionData = TestSubscriptionData(plan = Plan.Unlimited).annotationTestData + + @get:Rule + val rule: ProtonRule = protonRule( + testSubscriptionData, + envConfig = EnvironmentConfig(host = "proton.black"), + userData = TestUserData(name = randomUsername()), + loginBefore = true, + logoutBefore = true, + logoutAfter = true, + activityRule = activityScenarioRule(), + setUp = { } + ) + + @Test + fun defaultExample() { + // MainActivity is started with a seeded user with Unlimited plan logged in on 'proton.black' environment. + } + + @Test + @EnvironmentConfig(host = "proton.me") + @TestUserData(name = "pro", password = "pro", shouldSeed = false) + fun overrideExample() { + // MainActivity is started with a user 'pro' logged in on 'proton.me' environment. User will not be seeded beforehand + } + + + @Test + @TestSubscriptionData(plan = Plan.MailPlus) + fun overrideExample2() { + // MainActivity is started with a seeded user with MailPlus plan logged in on 'proton.black' environment. + } +} +``` + +**3. Helper Functions:** + +* `protonActivityScenarioRule()`: Sets up an `ActivityScenarioRule` for a specific activity type with optional setup logic. +* `protonAndroidComposeRule()`: Sets up a `ComposeTestRule` for a Compose activity with optional setup logic. + +These functions are useful for most common case scenarios: +```kotlin +@HiltAndroidTest +class TestClass { + + // If using Views + @get:Rule + val protonRule: ProtonRule = protonActivityScenarioRule( + val customTestData = TestFeatureEnabled(false).annotationTestData + ) { + MainInitializer.init(targetContext) + } + + // If using Compose + @get:Rule + val protonRule: ProtonRule = protonAndroidComposeRule( + val customTestData = TestFeatureEnabled(false).annotationTestData + ) { + MainInitializer.init(targetContext) + } + + @Test + @EnvironmentConfig(host = "scientist.proton.black") + fun example() { + // MainActivity is started with following configuration: + // 1. Default environment configuration provided by the application + // 2. Hilt test dependencies injected + // 3. Logged in with freshly created user with random username + // Can be accessed by protonRule.testDataRule.testUserData + // 4. All users are logged out before and after the test (to always assure clean state) + // 5. Default environment configuration is overriden by annotation + // 6. Test feature is disabled by default, but can be overriden by annotation @TestFeatureEnabled + } +} +``` + +**4. Custom Annotations:** + +Define custom annotations to capture test-specific configurations: + +```kotlin +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class TestFeatureEnabled(val enabled: Boolean = true) + +public val TestFeatureEnabled.annotationTestData: AnnotationTestData + get() = AnnotationTestData( + this, + implementation = { data -> + // Enable/disable the feature based on data.enabled + }, + tearDown = { _ -> + // Reset feature state to default + } + ) +``` + +**5. Using Annotations with Rules:** + +Combine annotations and rules to tailor test setups: + +```kotlin +val customTestData = TestFeatureEnabled(false).annotationTestData, // Disable specific feature +val otherTestData = OtherTestData().annotationTestData + +@Rule +override val protonRule: ProtonRule = protonActivityScenarioRule( + customData, otherTestData, // provided using 'vararg' + logoutAfter = false +) + +@Test +fun example() { + assertFalse(customTestData.enabled) +} + +@Test +@TestFeatureEnabled(enabled = true) +fun exampleOverride() { + assertTrue(customTestData.enabled) +} +``` + +**Advanced Usage:** + +For advanced setup you can use underlying test rules directly and/or combine them into your own rules: +```kotlin +class AdvancedTestSetup { + private val testUserData = TestUserData( + name = randomUsername(), + password = "password", + genKeys = TestUserData.GenKeys.RSA4096 + ) + + private val environmentConfig = EnvironmentConfig(host = "proton.black") + + private val userConfig = ProtonRule.UserConfig( + userData = testUserData, + loginBefore = true, + logoutBefore = true, + logoutAfter = true + ) + + private val testConfig = ProtonRule.TestConfig( + envConfig = environmentConfig, + annotationTestData = arrayOf(TestSubscriptionData(plan = Plan.Unlimited).annotationTestData), + activityRule = activityScenarioRule() + ) + + @get:Rule + val envConfigRule = EnvironmentConfigRule( + defaultConfig = environmentConfig + ) + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val testDataRule = QuarkTestDataRule( + annotationTestData = testConfig.annotationTestData, + initialTestUserData = userConfig.userData, + environmentConfig = { envConfigRule.config } + ) + + @get:Rule + val authenticationRule: AuthenticationRule = AuthenticationRule( + userConfig = userConfig + ) + + @get:Rule + val setupRule: ExternalResource = before { + MainInitializer.init(targetContext) + } + + @get:Rule + val activityRule = testConfig.activityRule + + @Test + @TestSubscriptionData(plan = Plan.Unlimited) + fun example() { + // Test will run under the same conditions as using protonActivityScenarioRule() + } +} +``` diff --git a/test/rule/api/test-rule.api b/test/rule/api/test-rule.api new file mode 100644 index 000000000..5bc84c727 --- /dev/null +++ b/test/rule/api/test-rule.api @@ -0,0 +1,202 @@ +public class hilt_aggregated_deps/_me_proton_core_test_rule_di_TestEnvironmentConfigModule { + public fun ()V +} + +public final class me/proton/core/test/rule/AuthenticationRule : org/junit/rules/TestWatcher { + public fun (Lme/proton/core/test/rule/ProtonRule$UserConfig;)V +} + +public final class me/proton/core/test/rule/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/test/rule/EnvironmentConfigRule : org/junit/rules/TestRule { + public fun ()V + public fun (Lme/proton/core/test/rule/annotation/EnvironmentConfig;)V + public synthetic fun (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 class me/proton/core/test/rule/ProtonRule : org/junit/rules/TestRule { + public fun (Lme/proton/core/test/rule/ProtonRule$UserConfig;Lme/proton/core/test/rule/ProtonRule$TestConfig;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V + public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement; + public final fun getTestDataRule ()Lme/proton/core/test/rule/QuarkTestDataRule; +} + +public final class me/proton/core/test/rule/ProtonRule$TestConfig { + public fun ()V + public fun (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;)V + public synthetic fun (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 ()V + public fun (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)V + public synthetic fun (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 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 ([Lme/proton/core/test/rule/annotation/AnnotationTestData;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 class me/proton/core/test/rule/QuarkTestDataRule$Companion { + public final fun getQuarkClientTimeout ()Ljava/util/concurrent/atomic/AtomicReference; +} + +public final class me/proton/core/test/rule/annotation/AnnotationTestData { + public fun (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V + public synthetic fun (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)V + public synthetic fun (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;)V + public synthetic fun (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 getTearDown ()Lkotlin/jvm/functions/Function4; +} + +public abstract interface annotation class me/proton/core/test/rule/annotation/EnvironmentConfig : java/lang/annotation/Annotation { + public static final field Companion Lme/proton/core/test/rule/annotation/EnvironmentConfig$Companion; + public abstract fun host ()Ljava/lang/String; +} + +public final class me/proton/core/test/rule/annotation/EnvironmentConfig$Companion { + public final fun fromConfiguration (Lme/proton/core/configuration/EnvironmentConfiguration;)Lme/proton/core/test/rule/annotation/EnvironmentConfig; +} + +public final class me/proton/core/test/rule/annotation/EnvironmentConfigKt { + public static final fun getConfigContractFieldsMap (Lme/proton/core/test/rule/annotation/EnvironmentConfig;)Ljava/util/Map; + public static final fun toEnvironmentConfiguration (Lme/proton/core/test/rule/annotation/EnvironmentConfig;)Lme/proton/core/configuration/EnvironmentConfiguration; +} + +public abstract interface annotation class me/proton/core/test/rule/annotation/TestPaymentMethods : java/lang/annotation/Annotation { + public abstract fun appStore ()Lme/proton/core/domain/entity/AppStore; + public abstract fun card ()Z + public abstract fun inApp ()Z + public abstract fun paypal ()Z +} + +public final class me/proton/core/test/rule/annotation/TestPaymentMethodsKt { + public static final fun getAnnotationTestData (Lme/proton/core/test/rule/annotation/TestPaymentMethods;)Lme/proton/core/test/rule/annotation/AnnotationTestData; +} + +public abstract interface annotation class me/proton/core/test/rule/annotation/TestSubscriptionData : java/lang/annotation/Annotation { + public abstract fun couponCode ()Ljava/lang/String; + public abstract fun delinquent ()Z + public abstract fun plan ()Lme/proton/core/test/quark/data/Plan; +} + +public final class me/proton/core/test/rule/annotation/TestSubscriptionDataKt { + public static final fun getAnnotationTestData (Lme/proton/core/test/rule/annotation/TestSubscriptionData;)Lme/proton/core/test/rule/annotation/AnnotationTestData; +} + +public abstract interface annotation class me/proton/core/test/rule/annotation/TestUserData : java/lang/annotation/Annotation { + 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 external ()Z + public abstract fun externalEmail ()Ljava/lang/String; + public abstract fun genKeys ()Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public abstract fun mailboxPassword ()Ljava/lang/String; + public abstract fun name ()Ljava/lang/String; + public abstract fun password ()Ljava/lang/String; + public abstract fun recoveryEmail ()Ljava/lang/String; + public abstract fun recoveryPhone ()Ljava/lang/String; + 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 final class me/proton/core/test/rule/annotation/TestUserData$Companion { + public final fun getWithRandomUsername ()Lme/proton/core/test/rule/annotation/TestUserData; + public final fun randomUsername ()Ljava/lang/String; +} + +public final class me/proton/core/test/rule/annotation/TestUserData$GenKeys : java/lang/Enum { + public static final field Curve25519 Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public static final field None Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public static final field RSA2048 Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public static final field RSA4096 Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; + public static fun values ()[Lme/proton/core/test/rule/annotation/TestUserData$GenKeys; +} + +public final class me/proton/core/test/rule/annotation/TestUserData$Status : java/lang/Enum { + public static final field Active Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static final field Admin Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static final field Deleted Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static final field Disabled Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static final field Super Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static final field VPNAdmin Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lme/proton/core/test/rule/annotation/TestUserData$Status; + public static fun values ()[Lme/proton/core/test/rule/annotation/TestUserData$Status; +} + +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 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 class me/proton/core/test/rule/di/TestEnvironmentConfigModule_ProvideEnvironmentConfigurationFactory : dagger/internal/Factory { + public fun ()V + public static fun create ()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 final class me/proton/core/test/rule/extension/QuarkCommandKt { + public static final fun seedTestUserData (Lme/proton/core/test/quark/v2/QuarkCommand;Lme/proton/core/test/rule/annotation/TestUserData;)Lme/proton/core/test/quark/response/CreateUserQuarkResponse; + public static final fun setPaymentMethods (Lme/proton/core/test/quark/v2/QuarkCommand;Lme/proton/core/test/rule/annotation/TestPaymentMethods;)Lokhttp3/Response; + public static final fun subscriptionCreate (Lme/proton/core/test/quark/v2/QuarkCommand;Lme/proton/core/test/rule/annotation/TestSubscriptionData;Ljava/lang/String;)Lokhttp3/Response; +} + +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; +} + diff --git a/test/rule/build.gradle.kts b/test/rule/build.gradle.kts new file mode 100644 index 000000000..3e4d38c35 --- /dev/null +++ b/test/rule/build.gradle.kts @@ -0,0 +1,53 @@ +import studio.forface.easygradle.dsl.* +import studio.forface.easygradle.dsl.android.* + +/* + * 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 + kotlin("plugin.serialization") +} + +protonCoverage.disabled.set(true) +publishOption.shouldBePublishedAsLib = true + +android { + namespace = "me.proton.core.test.rule" +} + +dependencies { + implementation( + project(Module.configurationData), + project(Module.configurationDaggerContentResolver), + project(Module.quark), + project(Module.authDomain), + project(Module.authPresentation), + `coroutines-android`, + okhttp, + junit, + `hilt-android-testing`, + `android-ktx`, + `serialization-json`, + `androidx-test-monitor`, + `android-test-runner`, + `compose-ui-test-junit`, + `junit-ktx` + ) +} \ No newline at end of file diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/AuthenticationRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/AuthenticationRule.kt new file mode 100644 index 000000000..71cd41eb5 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/AuthenticationRule.kt @@ -0,0 +1,71 @@ +/* + * 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.test.rule + +import android.annotation.SuppressLint +import android.app.Application +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 org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * A JUnit test rule for managing user authentication states before and after tests. + * It utilizes [LoginTestHelper] for performing login and logout operations + * based on the provided [ProtonRule.UserConfig]. + */ +@SuppressLint("RestrictedApi") +public class AuthenticationRule( + private val userConfig: ProtonRule.UserConfig, +) : TestWatcher() { + + private val loginTestHelper: LoginTestHelper by lazy { + protonTestEntryPoint.loginTestHelper + } + + private val protonTestEntryPoint by lazy { + EntryPointAccessors.fromApplication( + ApplicationProvider.getApplicationContext(), + ProtonTestEntryPoint::class.java + ) + } + + override fun starting(description: Description) { + if (userConfig.logoutBefore) { + logout() + } + + userConfig.userData?.let { + if (userConfig.loginBefore) + loginTestHelper.login(it.name, it.password) + } + } + + override fun finished(description: Description) { + if (userConfig.logoutAfter) { + logout() + } + } + + private fun logout() { + runCatching { loginTestHelper.logoutAll() } + } +} diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/EnvironmentConfigRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/EnvironmentConfigRule.kt new file mode 100644 index 000000000..7ba98452f --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/EnvironmentConfigRule.kt @@ -0,0 +1,58 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule + +import me.proton.core.configuration.EnvironmentConfiguration +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 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]. + */ +public class EnvironmentConfigRule( + private val defaultConfig: EnvironmentConfig = + EnvironmentConfig.fromConfiguration(provideEnvironmentConfiguration()) +) : TestRule { + + /** + * 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 + + /** + * 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 + } +} diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/ProtonRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/ProtonRule.kt new file mode 100644 index 000000000..4e003a96d --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/ProtonRule.kt @@ -0,0 +1,124 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule + +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.test.rule.di.TestEnvironmentConfigModule.provideEnvironmentConfiguration +import org.junit.rules.ExternalResource +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Base class for custom test rules in the Proton framework. + * + * Provides a structured approach to managing various aspects of test setup and execution: + * + * - **Environment configuration:** Manages environment configuration using `EnvironmentConfigRule`. + * - **Hilt dependencies:** Handles Hilt dependency injection using `HiltAndroidRule`. + * - **Test data:** Provides test data management through `QuarkTestDataRule`. + * - **User authentication:** Optionally enables user authentication with `AuthenticationRule` + * based on provided `TestUserData`. + * - **User-defined setup:** Allows executing custom setup logic before each test through a provided lambda function. + * + * @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. + */ +public open class ProtonRule( + private val userConfig: UserConfig, + private val testConfig: TestConfig, + private val hiltTestInstance: Any, + private val setup: () -> Any +) : TestRule { + + private val environmentConfigRule by lazy { + val envConfig = testConfig.envConfig ?: EnvironmentConfig.fromConfiguration(provideEnvironmentConfiguration()) + EnvironmentConfigRule(envConfig) + } + + private val hiltRule by lazy { + HiltAndroidRule(hiltTestInstance) + } + + public val testDataRule: QuarkTestDataRule by lazy { + QuarkTestDataRule( + *testConfig.annotationTestData, + initialTestUserData = userConfig.userData, + environmentConfig = { environmentConfigRule.config } + ) + } + + private val authenticationRule by lazy { + userConfig + .takeUnless { it.userData == null } + ?.let { + AuthenticationRule(it) + } + } + + private val setupRule by lazy { + before { setup() } + } + + private val ruleChain: RuleChain by lazy { + RuleChain.outerRule(environmentConfigRule) + .around(hiltRule) + .around(setupRule) + .around(testDataRule) + .aroundNullable(authenticationRule) + .aroundNullable(testConfig.activityRule) + } + + 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> = emptyArray(), + val activityRule: TestRule? = null, + ) + + private fun RuleChain.aroundNullable(rule: TestRule?): RuleChain { + return around(rule ?: return this) + } +} + +public fun T.before(block: suspend T.() -> Any): ExternalResource = + object : ExternalResource() { + override fun before() { + runBlocking { + block() + } + } + } diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt new file mode 100644 index 000000000..95dfbcf10 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/QuarkTestDataRule.kt @@ -0,0 +1,124 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule + +import me.proton.core.configuration.EnvironmentConfiguration +import me.proton.core.test.quark.response.CreateUserQuarkResponse +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.handleExternal +import me.proton.core.test.rule.annotation.toEnvironmentConfiguration +import me.proton.core.test.rule.extension.seedTestUserData +import okhttp3.OkHttpClient +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.toJavaDuration + +/** + * A JUnit test rule that manages test data setup and teardown using Quark commands for configuring test environments + * and user data. This rule facilitates dynamic application of test data and environment configurations based + * on annotations or provided initial settings, ensuring tests run with the required setup. + * + * @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. + */ +public class QuarkTestDataRule( + private vararg val annotationTestData: AnnotationTestData, + initialTestUserData: TestUserData?, + private val environmentConfig: () -> EnvironmentConfig +) : TestWatcher() { + + private lateinit var quarkCommand: QuarkCommand + + private val environmentConfiguration: EnvironmentConfiguration by lazy { + environmentConfig().toEnvironmentConfiguration() + } + + private var createdUserResponse: CreateUserQuarkResponse? = null + + private val testDataMap: MutableMap, Annotation> = mutableMapOf() + + /** + * 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 + + /** + * 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) + + testUserData = description.getAnnotation(TestUserData::class.java)?.handleExternal() ?: testUserData + + testUserData?.takeIf { it.shouldSeed }?.apply { + createdUserResponse = quarkCommand.seedTestUserData(this) + } + + annotationTestData.forEach { entry -> + val annotationData = description.getAnnotation(entry.annotationClass) ?: entry.default + entry.implementation(quarkCommand, annotationData, testUserData, createdUserResponse) + testDataMap[entry.annotationClass] = annotationData + } + } + + /** 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( + quarkCommand, + it, + testUserData, + createdUserResponse + ) + } + } + } + + /** Retrieves test data of a specific annotation type, if applied to the current test. **/ + @Suppress("UNCHECKED_CAST") + public fun getTestData(annotationClass: Class): T = testDataMap[annotationClass] as T + + private val AnnotationTestData.annotationClass: Class + get() = default.annotationClass.java + + public companion object { + public val quarkClientTimeout: AtomicReference = AtomicReference(60.seconds) + + private val quarkClient: OkHttpClient by lazy { + OkHttpClient.Builder() + .connectTimeout(quarkClientTimeout.get().toJavaDuration()) + .readTimeout(quarkClientTimeout.get().toJavaDuration()) + .writeTimeout(quarkClientTimeout.get().toJavaDuration()) + .build() + } + } +} \ No newline at end of file diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/AnnotationTestData.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/AnnotationTestData.kt new file mode 100644 index 000000000..fd2af2b5a --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/AnnotationTestData.kt @@ -0,0 +1,89 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule.annotation + +import me.proton.core.test.quark.response.CreateUserQuarkResponse +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 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( + public val default: T, + public val implementation: QuarkCommand.(T, TestUserData?, CreateUserQuarkResponse?) -> Any, + public val tearDown: (QuarkCommand.(T, TestUserData?, CreateUserQuarkResponse?) -> Unit)? = null, +) { + + /** + * Convenience constructor allowing specifying implementation without requiring test user data. + * + * @param default The default annotation instance to use. + * @param implementation A lambda function defining how to apply the test data using the provided Quark command + * and test data annotation. + * @param tearDown An optional lambda function defining how to tear down the test data after the test, + * using the provided Quark command and test data annotation. + */ + public constructor( + default: T, + implementation: QuarkCommand.(T) -> Any, + tearDown: (QuarkCommand.(T) -> Unit)? = null + ) : this( + default = default, + implementation = { data, _, _ -> implementation(data) }, + tearDown = { data, _, _ -> tearDown?.invoke(this, data) } + ) + + /** + * Convenience constructor allowing specifying implementation with only CreateUserQuarkResponse. + * + * @param default The default annotation instance to use. + * @param implementation A lambda function defining how to apply the test data using the provided Quark command + * and created user response. + * @param tearDown An optional lambda function defining how to tear down the test data after the test, + * using the provided Quark command and created user response. + */ + public constructor( + default: T, + implementation: QuarkCommand.(T, CreateUserQuarkResponse) -> Any, + tearDown: (QuarkCommand.(T, CreateUserQuarkResponse) -> Unit)? = null + ) : this( + default = default, + implementation = { data, _, seededUser -> + implementation( + data, + seededUser ?: error("Cannot seed test data - TestUserData is not provided.") + ) + }, + tearDown = { data, _, seededUser -> + tearDown?.invoke( + this, + data, + seededUser ?: error("Cannot tear down test data - TestUserData is not provided.") + ) + } + ) +} \ No newline at end of file diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/EnvironmentConfig.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/EnvironmentConfig.kt new file mode 100644 index 000000000..1004fef9c --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/EnvironmentConfig.kt @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule.annotation + +import me.proton.core.configuration.EnvironmentConfiguration + + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class EnvironmentConfig( + public val host: String +) { + public companion object { + public fun fromConfiguration(config: EnvironmentConfiguration): EnvironmentConfig = + EnvironmentConfig(config.host) + } +} + +public val EnvironmentConfig.configContractFieldsMap: Map + get() = mapOf(::host.name to host) + +public fun EnvironmentConfig.toEnvironmentConfiguration(): EnvironmentConfiguration = + EnvironmentConfiguration.fromMap(configContractFieldsMap) \ No newline at end of file diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestPaymentMethods.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestPaymentMethods.kt new file mode 100644 index 000000000..0fc9622fb --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestPaymentMethods.kt @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule.annotation + +import me.proton.core.domain.entity.AppStore +import me.proton.core.test.quark.v2.command.setPaymentMethods + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class TestPaymentMethods( + val appStore: AppStore = AppStore.GooglePlay, + val card: Boolean = true, + val paypal: Boolean = true, + val inApp: Boolean = true +) + +public val TestPaymentMethods.annotationTestData: AnnotationTestData + get() = AnnotationTestData( + this, + implementation = { data -> + setPaymentMethods( + data.appStore, + data.card, + data.paypal, + data.inApp + ) + }, + tearDown = { _ -> + setPaymentMethods( + card = true, + paypal = true, + inApp = true + ) + } + ) diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestSubscriptionData.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestSubscriptionData.kt new file mode 100644 index 000000000..5e3cdf8f2 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestSubscriptionData.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.test.rule.annotation + +import me.proton.core.test.quark.data.Plan +import me.proton.core.test.quark.response.CreateUserQuarkResponse +import me.proton.core.test.rule.extension.subscriptionCreate +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 couponCode: String = EMPTY_STRING, + val delinquent: Boolean = false +) + +public val TestSubscriptionData.annotationTestData: AnnotationTestData + get() = AnnotationTestData( + default = this, + implementation = { data: TestSubscriptionData, seededUser: CreateUserQuarkResponse? -> + seededUser?.let { + subscriptionCreate(data, it.decryptedUserId.toString()) + } ?: error("Could not create subscription. User is not seeded.") + } + ) diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestUserData.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestUserData.kt new file mode 100644 index 000000000..b14085fe6 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/annotation/TestUserData.kt @@ -0,0 +1,77 @@ +/* + * 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.test.rule.annotation + +import me.proton.core.util.kotlin.EMPTY_STRING +import me.proton.core.util.kotlin.random + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Suppress("LongParameterList") +public annotation class TestUserData( + val name: String = EMPTY_STRING, + val password: String = "password", + val recoveryEmail: String = EMPTY_STRING, + val status: Status = Status.Active, + val authVersion: Int = 4, + val createAddress: Boolean = true, + val genKeys: GenKeys = GenKeys.Curve25519, + val mailboxPassword: String = EMPTY_STRING, + val external: Boolean = false, + val toTpSecret: String = EMPTY_STRING, + val recoveryPhone: String = EMPTY_STRING, + val externalEmail: String = EMPTY_STRING, + + val shouldSeed: Boolean = true, +) { + public enum class Status { + Deleted, Disabled, Active, VPNAdmin, Admin, Super + } + + public enum class GenKeys { + None, RSA2048, RSA4096, Curve25519 + } + + public companion object { + public fun randomUsername(): String = + arrayOf(('A'..'Z').toList(), ('1'..'9').toList()) + .map { String.random(length = 4, charPool = it) } + .let { "proton${it[0]}${it[1]}" } + + public val withRandomUsername: TestUserData get() = TestUserData(randomUsername()) + } +} + +public fun TestUserData.handleExternal( + username: String = TestUserData.randomUsername(), + extEmail: String = "${TestUserData.randomUsername()}@example.lt" +): TestUserData = TestUserData( + name = if (name.isEmpty() && external) name else username, + password, + recoveryEmail, + status, + authVersion, + createAddress, + genKeys, + mailboxPassword, + external, + toTpSecret, + recoveryPhone, + externalEmail = if (externalEmail.isEmpty() && external) extEmail else externalEmail +) diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/di/TestEnvironmentConfigModule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/di/TestEnvironmentConfigModule.kt new file mode 100644 index 000000000..aed1f9b68 --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/di/TestEnvironmentConfigModule.kt @@ -0,0 +1,69 @@ +/* + * 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 . + */ + +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.EnvironmentConfiguration +import me.proton.core.configuration.dagger.ContentResolverEnvironmentConfigModule +import me.proton.core.configuration.extension.configContractFieldsMap +import java.util.concurrent.atomic.AtomicReference +import javax.inject.Singleton + +@Module +@TestInstallIn( + components = [SingletonComponent::class], + replaces = [ContentResolverEnvironmentConfigModule::class] +) +public object TestEnvironmentConfigModule { + @Provides + @Singleton + public fun provideEnvironmentConfiguration(): EnvironmentConfiguration = + InstrumentationRegistry + .getArguments() + .configFields() + .takeIf { it.isNotEmpty() } + ?.let { + EnvironmentConfiguration.fromMap(it) + } ?: EnvironmentConfiguration(::getConfigValue) + + + public val overrideConfig: AtomicReference = AtomicReference(null) + + private val defaultConfig = EnvironmentConfiguration.fromClass() + + private fun getConfigValue(key: String): String { + val defaultValue = defaultConfig.configContractFieldsMap[key].toString() + val overrideValue = overrideConfig.get()?.configContractFieldsMap?.get(key)?.toString() + return overrideValue ?: defaultValue + } + + private fun Bundle.configFields(): Map { + val hostKey = EnvironmentConfiguration::host.name + val proxyTokenKey = EnvironmentConfiguration::proxyToken.name + return mapOf( + hostKey to (getString(hostKey) ?: getConfigValue(hostKey)), + proxyTokenKey to (getString(proxyTokenKey) ?: getConfigValue(proxyTokenKey)) + ) + } +} diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/QuarkCommand.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/QuarkCommand.kt new file mode 100644 index 000000000..f86a062ac --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/QuarkCommand.kt @@ -0,0 +1,101 @@ +/* + * 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.test.rule.extension + +import kotlinx.serialization.StringFormat +import kotlinx.serialization.decodeFromString +import me.proton.core.test.quark.response.CreateUserQuarkResponse +import me.proton.core.test.quark.v2.QuarkCommand +import me.proton.core.test.quark.v2.command.USERS_CREATE +import me.proton.core.test.quark.v2.command.setPaymentMethods +import me.proton.core.test.quark.v2.toEncodedArgs +import me.proton.core.test.rule.annotation.TestPaymentMethods +import me.proton.core.test.rule.annotation.TestSubscriptionData +import me.proton.core.test.rule.annotation.TestUserData +import me.proton.core.util.kotlin.EMPTY_STRING +import okhttp3.Response + +public fun QuarkCommand.setPaymentMethods(methods: TestPaymentMethods): Response = + setPaymentMethods( + methods.appStore, + methods.card, + methods.paypal, + methods.inApp + ) + +public fun QuarkCommand.seedTestUserData(data: TestUserData): CreateUserQuarkResponse { + val args = listOf( + "-N" to data.name, + "-p" to data.password, + "-c" to data.createAddress.trueOrEmpty(), + "-r" to data.recoveryEmail, + "-s" to data.status.ordinal.toString(), + "-a" to data.authVersion.toString(), + "-c" to data.createAddress.trueOrEmpty(), + "-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, + "--format" to "json" + ).toEncodedArgs(ignoreEmpty = true) + + return route(USERS_CREATE) + .args(args) + .build() + .let { + client.executeQuarkRequest(it) + } + .let { + json.tryDecodeResponseBody(it) + } +} + +public fun QuarkCommand.subscriptionCreate( + subscription: TestSubscriptionData, + decryptedUserId: String +): Response = + route("quark/raw::user:create:subscription") + .args( + listOf( + "userID" to decryptedUserId, + "--planID" to subscription.plan.planName, + "--couponCode" to subscription.couponCode, + "--cycle" to subscription.delinquent.trueOrEmpty(), + "--format" to "json" + ).toEncodedArgs() + ) + .build() + .let { + client.executeQuarkRequest(it) + } + +private fun Boolean.trueOrEmpty() = if (this) toString() else EMPTY_STRING + +private fun TestUserData.GenKeys.valueOrEmpty() = if (this != TestUserData.GenKeys.None) toString() else EMPTY_STRING + +private fun StringFormat.tryDecodeResponseBody(response: Response): CreateUserQuarkResponse { + val responseString = response.body!!.string() + return try { + decodeFromString(responseString) + } catch (ex: Exception) { + error(responseString) + } +} diff --git a/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/TestRule.kt b/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/TestRule.kt new file mode 100644 index 000000000..4f9a7cb1c --- /dev/null +++ b/test/rule/src/main/kotlin/me/proton/core/test/rule/extension/TestRule.kt @@ -0,0 +1,161 @@ +/* + * 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 . + */ + +package me.proton.core.test.rule.extension + +import android.app.Activity +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.rules.activityScenarioRule +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 org.junit.rules.TestRule + +/** + * Creates a `ProtonRule` instance for managing test setup and execution. + * + * This function offers fine-grained control over various test aspects, including: + * - Environment configuration + * - Test data + * - User authentication + * - Custom setup logic + * - Activity or Compose test rule + * + * @param annotationTestData Array 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). + * @return A new `ProtonRule` instance. + */ +@SuppressWarnings("LongParameterList") +public fun Any.protonRule( + vararg annotationTestData: AnnotationTestData, + envConfig: EnvironmentConfig? = null, + userData: TestUserData? = null, + loginBefore: Boolean = false, + logoutBefore: Boolean = false, + logoutAfter: Boolean = false, + activityRule: TestRule? = null, + setUp: () -> Any = { }, +): ProtonRule { + val userConfig = ProtonRule.UserConfig( + userData = userData, + loginBefore = loginBefore, + logoutBefore = logoutBefore, + logoutAfter = logoutAfter + ) + + val testConfig = ProtonRule.TestConfig( + envConfig = envConfig, + annotationTestData = annotationTestData, + activityRule = activityRule + ) + + return ProtonRule( + userConfig = userConfig, + testConfig = testConfig, + hiltTestInstance = this, + setup = setUp + ) +} + +/** + * Creates a `ProtonRule` instance with an `ActivityScenarioRule` for a specific activity type. + * + * This function inherits all features of `protonRule` and uses the provided `ActivityScenarioRule` + * for managing the activity lifecycle within the test. + * + * @param A The type of activity to be used with the `ActivityScenarioRule`. + * @param annotationTestData Array 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: true). + * @param logoutBefore Whether to perform logout before the test (default: true). + * @param logoutAfter Whether to perform logout after the test (default: true). + * @param activityScenarioRule An `ActivityScenarioRule` for the specified activity type + * (default: created using `activityScenarioRule()`). + * @param setUp A lambda function containing setup logic to be executed before each test (default: empty). + * @return A new `ProtonRule` instance. + */ +@SuppressWarnings("LongParameterList") +public inline fun Any.protonActivityScenarioRule( + vararg annotationTestData: AnnotationTestData, + envConfig: EnvironmentConfig? = null, + userData: TestUserData? = TestUserData.withRandomUsername, + loginBefore: Boolean = true, + logoutBefore: Boolean = true, + logoutAfter: Boolean = true, + activityScenarioRule: ActivityScenarioRule = activityScenarioRule(), + noinline setUp: () -> Any = { }, +): ProtonRule = protonRule( + annotationTestData = annotationTestData, + envConfig = envConfig, + userData = userData, + loginBefore = loginBefore, + logoutBefore = logoutBefore, + logoutAfter = logoutAfter, + activityRule = activityScenarioRule, + setUp = setUp +) + +/** + * Creates a `ProtonRule` instance with a `ComposeTestRule` for an Android Compose component activity. + * + * This function inherits all features of `protonRule` and uses the provided `ComposeTestRule` + * for managing the Compose UI during the test. + * + * @param A The type of component activity to be used with the `ComposeTestRule`. + * @param annotationTestData Array 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: true). + * @param logoutBefore Whether to perform logout before the test (default: true). + * @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). + * @return A new `ProtonRule` instance. + */ +@SuppressWarnings("LongParameterList") +public inline fun Any.protonAndroidComposeRule( + vararg annotationTestData: AnnotationTestData, + envConfig: EnvironmentConfig? = null, + userData: TestUserData? = TestUserData.withRandomUsername, + loginBefore: Boolean = true, + logoutBefore: Boolean = true, + logoutAfter: Boolean = true, + composeTestRule: ComposeTestRule = createAndroidComposeRule(), + noinline setUp: () -> Any = { }, +): ProtonRule = protonRule( + annotationTestData = annotationTestData, + envConfig = envConfig, + userData = userData, + loginBefore = loginBefore, + logoutBefore = logoutBefore, + logoutAfter = logoutAfter, + activityRule = composeTestRule, + setUp = setUp +)