feat: Added test rules package.

This commit is contained in:
Artiom Košelev
2024-03-12 11:54:09 +00:00
parent 6b64cfe6f3
commit bc727821fb
18 changed files with 1466 additions and 5 deletions
+1
View File
@@ -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 |
+1 -5
View File
@@ -35,11 +35,7 @@ protonCoverage {
}
dependencies {
implementation(
project(Module.networkData),
datastore,
datastorePreferences,
)
implementation(project(Module.networkData))
testImplementation(junit, mockk)
}
+1
View File
@@ -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
+202
View File
@@ -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<MainActivity>(),
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<A>()`: Sets up an `ActivityScenarioRule` for a specific activity type with optional setup logic.
* `protonAndroidComposeRule<A>()`: 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<MainActivity>(
val customTestData = TestFeatureEnabled(false).annotationTestData
) {
MainInitializer.init(targetContext)
}
// If using Compose
@get:Rule
val protonRule: ProtonRule = protonAndroidComposeRule<MainActivity>(
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<TestFeatureEnabled>
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<MainActivity>(
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<MainActivity>()
)
@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()
}
}
```
+202
View File
@@ -0,0 +1,202 @@
public class hilt_aggregated_deps/_me_proton_core_test_rule_di_TestEnvironmentConfigModule {
public fun <init> ()V
}
public final class me/proton/core/test/rule/AuthenticationRule : org/junit/rules/TestWatcher {
public fun <init> (Lme/proton/core/test/rule/ProtonRule$UserConfig;)V
}
public 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 <init> ()V
}
public final class me/proton/core/test/rule/EnvironmentConfigRule : org/junit/rules/TestRule {
public fun <init> ()V
public fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;)V
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun apply (Lorg/junit/runners/model/Statement;Lorg/junit/runner/Description;)Lorg/junit/runners/model/Statement;
public final fun getConfig ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
}
public class me/proton/core/test/rule/ProtonRule : org/junit/rules/TestRule {
public fun <init> (Lme/proton/core/test/rule/ProtonRule$UserConfig;Lme/proton/core/test/rule/ProtonRule$TestConfig;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)V
public fun 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 <init> ()V
public fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;)V
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
public final fun component2 ()[Lme/proton/core/test/rule/annotation/AnnotationTestData;
public final fun component3 ()Lorg/junit/rules/TestRule;
public final fun copy (Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;)Lme/proton/core/test/rule/ProtonRule$TestConfig;
public static synthetic fun copy$default (Lme/proton/core/test/rule/ProtonRule$TestConfig;Lme/proton/core/test/rule/annotation/EnvironmentConfig;[Lme/proton/core/test/rule/annotation/AnnotationTestData;Lorg/junit/rules/TestRule;ILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule$TestConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getActivityRule ()Lorg/junit/rules/TestRule;
public final fun getAnnotationTestData ()[Lme/proton/core/test/rule/annotation/AnnotationTestData;
public final fun getEnvConfig ()Lme/proton/core/test/rule/annotation/EnvironmentConfig;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/test/rule/ProtonRule$UserConfig {
public fun <init> ()V
public fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)V
public synthetic fun <init> (Lme/proton/core/test/rule/annotation/TestUserData;ZZZILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Lme/proton/core/test/rule/annotation/TestUserData;
public final fun component2 ()Z
public final fun component3 ()Z
public final fun component4 ()Z
public final fun copy (Lme/proton/core/test/rule/annotation/TestUserData;ZZZ)Lme/proton/core/test/rule/ProtonRule$UserConfig;
public static synthetic fun copy$default (Lme/proton/core/test/rule/ProtonRule$UserConfig;Lme/proton/core/test/rule/annotation/TestUserData;ZZZILjava/lang/Object;)Lme/proton/core/test/rule/ProtonRule$UserConfig;
public fun equals (Ljava/lang/Object;)Z
public final fun getLoginBefore ()Z
public final fun getLogoutAfter ()Z
public final fun getLogoutBefore ()Z
public final fun getOverrideLogin ()Z
public final fun getUserData ()Lme/proton/core/test/rule/annotation/TestUserData;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class me/proton/core/test/rule/ProtonRuleKt {
public static final fun before (Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Lorg/junit/rules/ExternalResource;
}
public final class me/proton/core/test/rule/QuarkTestDataRule : org/junit/rules/TestWatcher {
public static final field Companion Lme/proton/core/test/rule/QuarkTestDataRule$Companion;
public fun <init> ([Lme/proton/core/test/rule/annotation/AnnotationTestData;Lme/proton/core/test/rule/annotation/TestUserData;Lkotlin/jvm/functions/Function0;)V
public 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 <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V
public synthetic fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;)V
public synthetic fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;)V
public synthetic fun <init> (Ljava/lang/annotation/Annotation;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getDefault ()Ljava/lang/annotation/Annotation;
public final fun getImplementation ()Lkotlin/jvm/functions/Function4;
public final fun 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 <init> ()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;
}
+53
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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`
)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Application>(),
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() }
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<out AnnotationTestData<Annotation>> = emptyArray(),
val activityRule: TestRule? = null,
)
private fun RuleChain.aroundNullable(rule: TestRule?): RuleChain {
return around(rule ?: return this)
}
}
public fun <T> T.before(block: suspend T.() -> Any): ExternalResource =
object : ExternalResource() {
override fun before() {
runBlocking {
block()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Annotation>,
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<Class<out Annotation>, 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 <T : Annotation> getTestData(annotationClass: Class<T>): T = testDataMap[annotationClass] as T
private val AnnotationTestData<Annotation>.annotationClass: Class<out Annotation>
get() = default.annotationClass.java
public companion object {
public val quarkClientTimeout: AtomicReference<Duration> = AtomicReference(60.seconds)
private val quarkClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(quarkClientTimeout.get().toJavaDuration())
.readTimeout(quarkClientTimeout.get().toJavaDuration())
.writeTimeout(quarkClientTimeout.get().toJavaDuration())
.build()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<T: Annotation>(
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.")
)
}
)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, String?>
get() = mapOf(::host.name to host)
public fun EnvironmentConfig.toEnvironmentConfiguration(): EnvironmentConfiguration =
EnvironmentConfiguration.fromMap(configContractFieldsMap)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TestPaymentMethods>
get() = AnnotationTestData(
this,
implementation = { data ->
setPaymentMethods(
data.appStore,
data.card,
data.paypal,
data.inApp
)
},
tearDown = { _ ->
setPaymentMethods(
card = true,
paypal = true,
inApp = true
)
}
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TestSubscriptionData>
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.")
}
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<EnvironmentConfiguration?> = 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<String, Any?> {
val hostKey = EnvironmentConfiguration::host.name
val proxyTokenKey = EnvironmentConfiguration::proxyToken.name
return mapOf(
hostKey to (getString(hostKey) ?: getConfigValue(hostKey)),
proxyTokenKey to (getString(proxyTokenKey) ?: getConfigValue(proxyTokenKey))
)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Annotation>,
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 <reified A : Activity> Any.protonActivityScenarioRule(
vararg annotationTestData: AnnotationTestData<Annotation>,
envConfig: EnvironmentConfig? = null,
userData: TestUserData? = TestUserData.withRandomUsername,
loginBefore: Boolean = true,
logoutBefore: Boolean = true,
logoutAfter: Boolean = true,
activityScenarioRule: ActivityScenarioRule<A> = 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 <reified A : ComponentActivity> Any.protonAndroidComposeRule(
vararg annotationTestData: AnnotationTestData<Annotation>,
envConfig: EnvironmentConfig? = null,
userData: TestUserData? = TestUserData.withRandomUsername,
loginBefore: Boolean = true,
logoutBefore: Boolean = true,
logoutAfter: Boolean = true,
composeTestRule: ComposeTestRule = createAndroidComposeRule<A>(),
noinline setUp: () -> Any = { },
): ProtonRule = protonRule(
annotationTestData = annotationTestData,
envConfig = envConfig,
userData = userData,
loginBefore = loginBefore,
logoutBefore = logoutBefore,
logoutAfter = logoutAfter,
activityRule = composeTestRule,
setUp = setUp
)