diff options
Diffstat (limited to 'android')
24 files changed, 362 insertions, 110 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index e7403733d0..c9ccec1cd0 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -243,7 +243,7 @@ androidComponents { variantBuilder.enable = variantBuilder.let { currentVariant -> val enabledVariants = - enabledVariantTriples.map { (billing, infra, buildType) -> + enabledAppVariantTriples.map { (billing, infra, buildType) -> billing + infra.capitalized() + buildType.capitalized() } enabledVariants.contains(currentVariant.name) diff --git a/android/buildSrc/src/main/kotlin/BuildVariants.kt b/android/buildSrc/src/main/kotlin/BuildVariants.kt index 476efd34e9..4595cf5b9d 100644 --- a/android/buildSrc/src/main/kotlin/BuildVariants.kt +++ b/android/buildSrc/src/main/kotlin/BuildVariants.kt @@ -33,7 +33,7 @@ object Flavors { const val STAGEMOLE = "stagemole" } -val enabledVariantTriples = +val enabledAppVariantTriples = listOf( Triple(OSS, PROD, DEBUG), Triple(OSS, PROD, RELEASE), @@ -46,3 +46,9 @@ val enabledVariantTriples = Triple(PLAY, STAGEMOLE, DEBUG), Triple(PLAY, STAGEMOLE, RELEASE) ) + +val enabledE2eVariantTriples = + listOf( + Triple(OSS, PROD, DEBUG), + Triple(PLAY, STAGEMOLE, DEBUG) + ) diff --git a/android/scripts/run-instrumented-tests.sh b/android/scripts/run-instrumented-tests.sh index 3d25480854..5e3320533f 100755 --- a/android/scripts/run-instrumented-tests.sh +++ b/android/scripts/run-instrumented-tests.sh @@ -9,64 +9,145 @@ AUTO_FETCH_TEST_HELPER_APKS=${AUTO_FETCH_TEST_HELPER_APKS:-"false"} APK_BASE_DIR=${APK_BASE_DIR:-"$SCRIPT_DIR/.."} LOG_FAILURE_MESSAGE="FAILURES!!!" + DEFAULT_ORCHESTRATOR_APK_PATH=/tmp/orchestrator.apk -ORCHESTRATOR_URL=https://dl.google.com/android/maven2/androidx/test/orchestrator/1.4.2/orchestrator-1.4.2.apk DEFAULT_TEST_SERVICES_APK_PATH=/tmp/test-services.apk + +ORCHESTRATOR_URL=https://dl.google.com/android/maven2/androidx/test/orchestrator/1.4.2/orchestrator-1.4.2.apk TEST_SERVICES_URL=https://dl.google.com/android/maven2/androidx/test/services/test-services/1.4.2/test-services-1.4.2.apk +PARTNER_AUTH="${PARTNER_AUTH:-}" +VALID_TEST_ACCOUNT_TOKEN="${VALID_TEST_ACCOUNT_TOKEN:-}" +INVALID_TEST_ACCOUNT_TOKEN="${INVALID_TEST_ACCOUNT_TOKEN:-}" + while [[ "$#" -gt 0 ]]; do case $1 in - app) - TEST_TYPE="app" - USE_ORCHESTRATOR="false" - TEST_PACKAGE="net.mullvad.mullvadvpn.test" - TEST_APK="$APK_BASE_DIR/app/build/outputs/apk/androidTest/ossProd/debug/app-oss-prod-debug-androidTest.apk" - ;; - e2e) - TEST_TYPE="e2e" - USE_ORCHESTRATOR="true" - TEST_PACKAGE="net.mullvad.mullvadvpn.test.$TEST_TYPE" - TEST_APK="$APK_BASE_DIR/test/$TEST_TYPE/build/outputs/apk/debug/$TEST_TYPE-debug.apk" - if [[ -z ${VALID_TEST_ACCOUNT_TOKEN-} ]]; then - echo "The variable VALID_TEST_ACCOUNT_TOKEN is not set." + --test-type) + if [[ ! -z ${2-} && "$2" =~ ^(app|mockapi|e2e)$ ]]; then + TEST_TYPE="$2" + else + echo "Error: Bad or missing test type. Must be one of: app, mockapi, e2e" exit 1 fi - if [[ -z ${INVALID_TEST_ACCOUNT_TOKEN-} ]]; then - echo "The variable INVALID_TEST_ACCOUNT_TOKEN is not set." + shift 2 + ;; + --infra-flavor) + if [[ ! -z ${2-} && "$2" =~ ^(prod|stagemole)$ ]]; then + INFRA_FLAVOR="$2" + else + echo "Error: Bad or missing infra flavor. Must be one of: prod, stagemole" exit 1 fi - OPTIONAL_TEST_ARGUMENTS="\ - -e valid_test_account_token $VALID_TEST_ACCOUNT_TOKEN \ - -e invalid_test_account_token $INVALID_TEST_ACCOUNT_TOKEN" + shift 2 ;; - mockapi) - TEST_TYPE="mockapi" - USE_ORCHESTRATOR="true" - TEST_PACKAGE="net.mullvad.mullvadvpn.test.$TEST_TYPE" - TEST_APK="$APK_BASE_DIR/test/$TEST_TYPE/build/outputs/apk/debug/$TEST_TYPE-debug.apk" + --billing-flavor) + if [[ ! -z ${2-} && "$2" =~ ^(oss|play)$ ]]; then + BILLING_FLAVOR="$2" + else + echo "Error: Bad or missing billing flavor. Must be one of: oss, play" + exit 1 + fi + shift 2 ;; *) echo "Unknown argument: $1" exit 1 ;; esac - shift done if [[ -z ${TEST_TYPE-} ]]; then - echo "Missing test type argument. Should be one of: app, e2e, mockapi" + echo "Error: Missing --test-type argument. Must be set to one of: app, e2e, mockapi" exit 1 fi +if [[ -z ${INFRA_FLAVOR-} ]]; then + echo "Error: Missing --infra-flavor argument. Must be set to one of: prod, stagemole" + exit 1 +fi + +if [[ -z ${BILLING_FLAVOR-} ]]; then + echo "Error: Missing --billing-flavor argument. Must be set to one of: oss, play" + exit 1 +fi + +echo "### Configuration ###" +echo "Test type: $TEST_TYPE" +echo "Infra flavor: $INFRA_FLAVOR" +echo "Billing flavor: $BILLING_FLAVOR" + +APK_PATH="$APK_BASE_DIR/app/build/outputs/apk/$BILLING_FLAVOR${INFRA_FLAVOR^}/debug/app-$BILLING_FLAVOR-$INFRA_FLAVOR-debug.apk" + +case "$TEST_TYPE" in + app) + if [[ $BILLING_FLAVOR != "oss" || $INFRA_FLAVOR != "prod" ]]; then + echo "" + echo "Error: The 'app' test type only supports billing type 'oss' and infra type 'prod'." + exit 1 + fi + USE_ORCHESTRATOR="false" + PACKAGE_NAME="net.mullvad.mullvadvpn" + TEST_PACKAGE_NAME="net.mullvad.mullvadvpn.test" + TEST_APK_PATH="$APK_BASE_DIR/app/build/outputs/apk/androidTest/$BILLING_FLAVOR${INFRA_FLAVOR^}/debug/app-$BILLING_FLAVOR-$INFRA_FLAVOR-debug-androidTest.apk" + ;; + mockapi) + + if [[ $BILLING_FLAVOR != "oss" || $INFRA_FLAVOR != "prod" ]]; then + echo "" + echo "Error: The 'mockapi' test type only supports billing type 'oss' and infra type 'prod'." + exit 1 + fi + USE_ORCHESTRATOR="true" + PACKAGE_NAME="net.mullvad.mullvadvpn" + TEST_PACKAGE_NAME="net.mullvad.mullvadvpn.test.mockapi" + TEST_APK_PATH="$APK_BASE_DIR/test/mockapi/build/outputs/apk/$BILLING_FLAVOR/debug/mockapi-$BILLING_FLAVOR-debug.apk" + ;; + + e2e) + if [[ $BILLING_FLAVOR == "play" && $INFRA_FLAVOR != "stagemole" ]]; then + echo "" + echo "Error: The 'e2e' test type with billing flavor 'play' require infra flavor 'stagemole'." + exit 1 + elif [[ $BILLING_FLAVOR == "oss" && $INFRA_FLAVOR != "prod" ]]; then + echo "" + echo "Error: The 'e2e' test type with billing flavor 'oss' require infra flavor 'prod'." + exit 1 + fi + OPTIONAL_TEST_ARGUMENTS="" + if [[ -n ${INVALID_TEST_ACCOUNT_TOKEN-} ]]; then + OPTIONAL_TEST_ARGUMENTS+=" -e invalid_test_account_token $INVALID_TEST_ACCOUNT_TOKEN" + else + echo "Error: The variable INVALID_TEST_ACCOUNT_TOKEN must be set." + exit 1 + fi + if [[ -n ${PARTNER_AUTH} ]]; then + echo "Test account used for e2e test (provided/partner): partner" + OPTIONAL_TEST_ARGUMENTS+=" -e partner_auth $PARTNER_AUTH" + elif [[ -n ${VALID_TEST_ACCOUNT_TOKEN} ]]; then + echo "Test account used for e2e test (provided/partner): provided" + OPTIONAL_TEST_ARGUMENTS+=" -e valid_test_account_token $VALID_TEST_ACCOUNT_TOKEN" + else + echo "" + echo "Error: The variable PARTNER_AUTH or VALID_TEST_ACCOUNT_TOKEN must be set." + exit 1 + fi + USE_ORCHESTRATOR="true" + PACKAGE_NAME="net.mullvad.mullvadvpn" + if [[ "$INFRA_FLAVOR" =~ ^(devmole|stagemole)$ ]]; then + PACKAGE_NAME+=".$INFRA_FLAVOR" + fi + TEST_PACKAGE_NAME="net.mullvad.mullvadvpn.test.e2e" + TEST_APK_PATH="$APK_BASE_DIR/test/e2e/build/outputs/apk/$BILLING_FLAVOR${INFRA_FLAVOR^}/debug/e2e-$BILLING_FLAVOR-$INFRA_FLAVOR-debug.apk" + ;; +esac + LOCAL_TMP_REPORT_PATH="/tmp/mullvad-$TEST_TYPE-instrumentation-report" INSTRUMENTATION_LOG_FILE_PATH="$LOCAL_TMP_REPORT_PATH/instrumentation-log.txt" LOGCAT_FILE_PATH="$LOCAL_TMP_REPORT_PATH/logcat.txt" LOCAL_SCREENSHOT_PATH="$LOCAL_TMP_REPORT_PATH/screenshots" DEVICE_SCREENSHOT_PATH="/sdcard/Pictures/mullvad-$TEST_TYPE" -echo "Preparing to run tests of type: $TEST_TYPE" echo "" - echo "### Ensure clean report structure ###" rm -rf "$LOCAL_TMP_REPORT_PATH" || echo "No report path" adb logcat --clear @@ -95,8 +176,8 @@ if [[ "${USE_ORCHESTRATOR-}" == "true" ]]; then fi echo "### Ensure that packages are not previously installed ###" -adb uninstall net.mullvad.mullvadvpn || echo "App package not installed" -adb uninstall "$TEST_PACKAGE" || echo "Test package not installed" +adb uninstall "$PACKAGE_NAME" || echo "App package not installed" +adb uninstall "$TEST_PACKAGE_NAME" || echo "Test package not installed" adb uninstall androidx.test.services || echo "Test services package not installed" adb uninstall androidx.test.orchestrator || echo "Test orchestrator package not installed" echo "" @@ -105,8 +186,8 @@ echo "Starting instrumented tests of type: $TEST_TYPE" echo "" echo "### Install packages ###" -adb install -t "$APK_BASE_DIR/app/build/outputs/apk/ossProd/debug/app-oss-prod-debug.apk" -adb install "$TEST_APK" +adb install -t "$APK_PATH" +adb install "$TEST_APK_PATH" if [[ "$USE_ORCHESTRATOR" == "true" ]]; then echo "Using ORCHESTRATOR_APK_PATH: $ORCHESTRATOR_APK_PATH" adb install "$ORCHESTRATOR_APK_PATH" @@ -120,7 +201,7 @@ if [[ "$USE_ORCHESTRATOR" == "true" ]]; then INSTRUMENTATION_COMMAND="\ CLASSPATH=\$(pm path androidx.test.services) app_process / androidx.test.services.shellexecutor.ShellMain \ am instrument -r -w \ - -e targetInstrumentation $TEST_PACKAGE/androidx.test.runner.AndroidJUnitRunner \ + -e targetInstrumentation $TEST_PACKAGE_NAME/androidx.test.runner.AndroidJUnitRunner \ -e clearPackageData true \ -e runnerBuilder de.mannodermaus.junit5.AndroidJUnit5Builder \ ${OPTIONAL_TEST_ARGUMENTS:-""} \ @@ -129,14 +210,14 @@ else INSTRUMENTATION_COMMAND="\ am instrument -w \ -e runnerBuilder de.mannodermaus.junit5.AndroidJUnit5Builder \ - $TEST_PACKAGE/androidx.test.runner.AndroidJUnitRunner" + $TEST_PACKAGE_NAME/androidx.test.runner.AndroidJUnitRunner" fi adb shell "$INSTRUMENTATION_COMMAND" | tee "$INSTRUMENTATION_LOG_FILE_PATH" echo "" echo "### Ensure that packages are uninstalled ###" -adb uninstall net.mullvad.mullvadvpn || echo "App package not installed" -adb uninstall "$TEST_PACKAGE" || echo "Test package not installed" +adb uninstall "$PACKAGE_NAME" || echo "App package not installed" +adb uninstall "$TEST_PACKAGE_NAME" || echo "Test package not installed" adb uninstall androidx.test.services || echo "Test services package not installed" adb uninstall androidx.test.orchestrator || echo "Test orchestrator package not installed" echo "" diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/annotation/SkipForFlavors.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/annotation/SkipForFlavors.kt new file mode 100644 index 0000000000..f1d3a990a1 --- /dev/null +++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/annotation/SkipForFlavors.kt @@ -0,0 +1,25 @@ +package net.mullvad.mullvadvpn.test.common.annotation + +import org.junit.jupiter.api.extension.ConditionEvaluationResult +import org.junit.jupiter.api.extension.ExecutionCondition +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.ExtensionContext + +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(SkipForFlavors.FlavorCondition::class) +annotation class SkipForFlavors(val currentFlavor: String, vararg val skipForFlavors: String) { + class FlavorCondition : ExecutionCondition { + override fun evaluateExecutionCondition(p0: ExtensionContext?): ConditionEvaluationResult { + val annotation = p0?.element?.get()?.getAnnotation(SkipForFlavors::class.java) + return if (annotation?.skipForFlavors?.contains(annotation.currentFlavor) == true) { + ConditionEvaluationResult.disabled( + "Skipping test for flavor: ${annotation.currentFlavor}" + ) + } else { + ConditionEvaluationResult.enabled( + "Running test for flavor: ${annotation?.currentFlavor}" + ) + } + } + } +} diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/constant/AppConstants.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/constant/AppConstants.kt deleted file mode 100644 index 05b47ef99b..0000000000 --- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/constant/AppConstants.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.test.common.constant - -const val MULLVAD_PACKAGE = "net.mullvad.mullvadvpn" -const val SETTINGS_COG_ID = "net.mullvad.mullvadvpn:id/settings" -const val TUNNEL_INFO_ID = "net.mullvad.mullvadvpn:id/tunnel_info" -const val TUNNEL_OUT_ADDRESS_ID = "net.mullvad.mullvadvpn:id/out_address" diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt index 1608f28bcf..fd976887ce 100644 --- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt +++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/interactor/AppInteractor.kt @@ -13,12 +13,15 @@ import net.mullvad.mullvadvpn.test.common.constant.CONNECTION_TIMEOUT import net.mullvad.mullvadvpn.test.common.constant.DEFAULT_INTERACTION_TIMEOUT import net.mullvad.mullvadvpn.test.common.constant.LOGIN_PROMPT_TIMEOUT import net.mullvad.mullvadvpn.test.common.constant.LOGIN_TIMEOUT -import net.mullvad.mullvadvpn.test.common.constant.MULLVAD_PACKAGE import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaimer import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout -class AppInteractor(private val device: UiDevice, private val targetContext: Context) { +class AppInteractor( + private val device: UiDevice, + private val targetContext: Context, + private val targetPackageName: String +) { fun launch(customApiEndpointConfiguration: CustomApiEndpointConfiguration? = null) { device.pressHome() // Wait for launcher @@ -28,7 +31,7 @@ class AppInteractor(private val device: UiDevice, private val targetContext: Con ) val intent = - targetContext.packageManager.getLaunchIntentForPackage(MULLVAD_PACKAGE)?.apply { + targetContext.packageManager.getLaunchIntentForPackage(targetPackageName)?.apply { // Clear out any previous instances addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) if (customApiEndpointConfiguration != null) { @@ -36,7 +39,7 @@ class AppInteractor(private val device: UiDevice, private val targetContext: Con } } targetContext.startActivity(intent) - device.wait(Until.hasObject(By.pkg(MULLVAD_PACKAGE).depth(0)), APP_LAUNCH_TIMEOUT) + device.wait(Until.hasObject(By.pkg(targetPackageName).depth(0)), APP_LAUNCH_TIMEOUT) } fun launchAndEnsureLoggedIn(accountToken: String) { diff --git a/android/test/e2e/build.gradle.kts b/android/test/e2e/build.gradle.kts index 946e8effa9..8440967aa9 100644 --- a/android/test/e2e/build.gradle.kts +++ b/android/test/e2e/build.gradle.kts @@ -1,5 +1,6 @@ import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import java.util.Properties +import org.gradle.configurationcache.extensions.capitalized plugins { id(Dependencies.Plugin.androidTestId) @@ -19,9 +20,6 @@ android { "de.mannodermaus.junit5.AndroidJUnit5Builder" targetProjectPath = ":app" - missingDimensionStrategy(FlavorDimensions.BILLING, Flavors.OSS) - missingDimensionStrategy(FlavorDimensions.INFRASTRUCTURE, Flavors.PROD) - fun Properties.addRequiredPropertyAsBuildConfigField(name: String) { val value = getProperty(name) ?: throw GradleException("Missing property: $name") buildConfigField(type = "String", name = name, value = "\"$value\"") @@ -29,7 +27,6 @@ android { Properties().apply { load(project.file("e2e.properties").inputStream()) - addRequiredPropertyAsBuildConfigField("API_BASE_URL") addRequiredPropertyAsBuildConfigField("API_VERSION") } @@ -51,6 +48,30 @@ android { } } + flavorDimensions += FlavorDimensions.BILLING + flavorDimensions += FlavorDimensions.INFRASTRUCTURE + + productFlavors { + create(Flavors.OSS) { dimension = FlavorDimensions.BILLING } + create(Flavors.PLAY) { dimension = FlavorDimensions.BILLING } + create(Flavors.PROD) { + dimension = FlavorDimensions.INFRASTRUCTURE + buildConfigField( + type = "String", + name = "INFRASTRUCTURE_BASE_DOMAIN", + value = "\"mullvad.net\"" + ) + } + create(Flavors.STAGEMOLE) { + dimension = FlavorDimensions.INFRASTRUCTURE + buildConfigField( + type = "String", + name = "INFRASTRUCTURE_BASE_DOMAIN", + value = "\"stagemole.eu\"" + ) + } + } + testOptions { execution = "ANDROIDX_TEST_ORCHESTRATOR" } compileOptions { @@ -78,6 +99,19 @@ android { } } +androidComponents { + beforeVariants { variantBuilder -> + variantBuilder.enable = + variantBuilder.let { currentVariant -> + val enabledVariants = + enabledE2eVariantTriples.map { (billing, infra, buildType) -> + billing + infra.capitalized() + buildType.capitalized() + } + enabledVariants.contains(currentVariant.name) + } + } +} + configure<org.owasp.dependencycheck.gradle.extension.DependencyCheckExtension> { // Skip the lintClassPath configuration, which relies on many dependencies that has been flagged // to have CVEs, as it's related to the lint tooling rather than the project's compilation class diff --git a/android/test/e2e/e2e.properties b/android/test/e2e/e2e.properties index f9420786c8..58798ef1b6 100644 --- a/android/test/e2e/e2e.properties +++ b/android/test/e2e/e2e.properties @@ -1,2 +1 @@ -API_BASE_URL=https://api.mullvad.net API_VERSION=v1 diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt index bbecb037f3..d66ee8a37c 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt @@ -3,25 +3,40 @@ package net.mullvad.mullvadvpn.test.e2e import androidx.test.uiautomator.By import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout import net.mullvad.mullvadvpn.test.common.rule.ForgetAllVpnAppsInSettingsTestRule -import net.mullvad.mullvadvpn.test.e2e.misc.CleanupAccountTestRule +import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule import net.mullvad.mullvadvpn.test.e2e.misc.ConnCheckState import net.mullvad.mullvadvpn.test.e2e.misc.SimpleMullvadHttpClient import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -class ConnectionTest : EndToEndTest() { +class ConnectionTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { - @RegisterExtension @JvmField val cleanupAccountTestRule = CleanupAccountTestRule() + @RegisterExtension @JvmField val accountTestRule = AccountTestRule() @RegisterExtension @JvmField val forgetAllVpnAppsInSettingsTestRule = ForgetAllVpnAppsInSettingsTestRule() @Test + fun testConnect() { + // Given + app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) + + // When + device.findObjectWithTimeout(By.text("Secure my connection")).click() + device.findObjectWithTimeout(By.text("OK")).click() + + // Then + device.findObjectWithTimeout(By.text("SECURE CONNECTION")) + } + + @Test + @Disabled("Disabled since the connection check isn't reliable in the stagemole infrastructure.") fun testConnectAndVerifyWithConnectionCheck() { // Given - app.launchAndEnsureLoggedIn(validTestAccountToken) + app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) // When device.findObjectWithTimeout(By.text("Secure my connection")).click() diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt index 2cf8ba712d..f4979258f2 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt @@ -8,14 +8,11 @@ import androidx.test.uiautomator.UiDevice import de.mannodermaus.junit5.extensions.GrantPermissionExtension import net.mullvad.mullvadvpn.test.common.interactor.AppInteractor import net.mullvad.mullvadvpn.test.common.rule.CaptureScreenshotOnFailedTestRule -import net.mullvad.mullvadvpn.test.e2e.constant.INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY import net.mullvad.mullvadvpn.test.e2e.constant.LOG_TAG -import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY -import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.extension.RegisterExtension -abstract class EndToEndTest { +abstract class EndToEndTest(private val infra: String) { @RegisterExtension @JvmField val rule = CaptureScreenshotOnFailedTestRule(LOG_TAG) @@ -34,21 +31,19 @@ abstract class EndToEndTest { lateinit var device: UiDevice lateinit var targetContext: Context lateinit var app: AppInteractor - lateinit var validTestAccountToken: String - lateinit var invalidTestAccountToken: String @BeforeEach fun setup() { device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) targetContext = InstrumentationRegistry.getInstrumentation().targetContext - validTestAccountToken = - InstrumentationRegistry.getArguments() - .getRequiredArgument(VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY) - invalidTestAccountToken = - InstrumentationRegistry.getArguments() - .getRequiredArgument(INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY) + val targetPackageNameSuffix = + when (infra) { + "devmole" -> ".devmole" + "stagemole" -> ".stagemole" + else -> "" + } - app = AppInteractor(device, targetContext) + app = AppInteractor(device, targetContext, "net.mullvad.mullvadvpn$targetPackageNameSuffix") } } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt index f68df92854..724bffcad0 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt @@ -2,7 +2,7 @@ package net.mullvad.mullvadvpn.test.e2e import org.junit.jupiter.api.Test -class LaunchAppTest : EndToEndTest() { +class LaunchAppTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @Test fun testLaunchApp() { app.launch() diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt index 792c63f1a1..9380589709 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt @@ -5,39 +5,41 @@ import net.mullvad.mullvadvpn.test.common.constant.LOGIN_FAILURE_TIMEOUT import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaimer import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout -import net.mullvad.mullvadvpn.test.e2e.misc.CleanupAccountTestRule +import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -class LoginTest : EndToEndTest() { +class LoginTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { - @RegisterExtension @JvmField val cleanupAccountTestRule = CleanupAccountTestRule() + @RegisterExtension @JvmField val accountTestRule = AccountTestRule() @Test - fun testLoginWithInvalidCredentials() { + fun testLoginWithValidCredentials() { // Given - val invalidDummyAccountToken = invalidTestAccountToken + val validTestAccountToken = accountTestRule.validAccountNumber // When - app.launch() - device.clickAgreeOnPrivacyDisclaimer() - device.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove() - app.waitForLoginPrompt() - app.attemptLogin(invalidDummyAccountToken) + app.launchAndEnsureLoggedIn(validTestAccountToken) // Then - device.findObjectWithTimeout(By.text("Invalid account number"), LOGIN_FAILURE_TIMEOUT) + app.ensureLoggedIn() } @Test - fun testLoginWithValidCredentials() { + @Disabled("Disabled to avoid getting rate-limited.") + fun testLoginWithInvalidCredentials() { // Given - val token = validTestAccountToken + val invalidDummyAccountToken = accountTestRule.invalidAccountNumber // When - app.launchAndEnsureLoggedIn(token) + app.launch() + device.clickAgreeOnPrivacyDisclaimer() + device.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove() + app.waitForLoginPrompt() + app.attemptLogin(invalidDummyAccountToken) // Then - app.ensureLoggedIn() + device.findObjectWithTimeout(By.text("Invalid account number"), LOGIN_FAILURE_TIMEOUT) } } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LogoutTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LogoutTest.kt index 95ad9c22f5..4d77c2a33a 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LogoutTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LogoutTest.kt @@ -2,19 +2,19 @@ package net.mullvad.mullvadvpn.test.e2e import androidx.test.uiautomator.By import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout -import net.mullvad.mullvadvpn.test.e2e.misc.CleanupAccountTestRule +import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.RegisterExtension -class LogoutTest : EndToEndTest() { +class LogoutTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { - @RegisterExtension @JvmField val cleanupAccountTestRule = CleanupAccountTestRule() + @RegisterExtension @JvmField val accountTestRule = AccountTestRule() @Test fun testLogout() { // Given - app.launchAndEnsureLoggedIn(validTestAccountToken) + app.launchAndEnsureLoggedIn(accountTestRule.validAccountNumber) // When app.clickAccountCog() diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt index 5e72305efe..d4caa1da56 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt @@ -1,14 +1,16 @@ package net.mullvad.mullvadvpn.test.e2e import androidx.test.uiautomator.By +import net.mullvad.mullvadvpn.test.common.annotation.SkipForFlavors import net.mullvad.mullvadvpn.test.common.constant.WEB_TIMEOUT import net.mullvad.mullvadvpn.test.common.extension.clickAgreeOnPrivacyDisclaimer import net.mullvad.mullvadvpn.test.common.extension.clickAllowOnNotificationPermissionPromptIfApiLevel33AndAbove import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout import org.junit.jupiter.api.Test -class WebLinkTest : EndToEndTest() { +class WebLinkTest : EndToEndTest(BuildConfig.FLAVOR_infrastructure) { @Test + @SkipForFlavors(currentFlavor = BuildConfig.FLAVOR_billing, "play") fun testOpenFaqFromApp() { // Given app.launch() diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ConnCheckConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ConnCheckConstants.kt deleted file mode 100644 index 5357ce0e75..0000000000 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ConnCheckConstants.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.mullvad.mullvadvpn.test.e2e.constant - -const val CONN_CHECK_URL = "https://am.i.mullvad.net/json" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt index 98fd52a333..dec8f1e07f 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.test.e2e.constant const val LOG_TAG = "mullvad-e2e" +const val PARTNER_AUTH = "partner_auth" const val VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY = "valid_test_account_token" const val INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY = "invalid_test_account_token" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt new file mode 100644 index 0000000000..70b06f9697 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/UrlConstants.kt @@ -0,0 +1,17 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +import net.mullvad.mullvadvpn.test.e2e.BuildConfig + +// API URLs +const val API_BASE_URL = "https://api.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}" +const val AUTH_URL = "$API_BASE_URL/auth/${BuildConfig.API_VERSION}/token" +const val ACCOUNT_URL = "$API_BASE_URL/accounts/${BuildConfig.API_VERSION}/accounts" +const val DEVICE_LIST_URL = "$API_BASE_URL/accounts/${BuildConfig.API_VERSION}/devices" + +// Partner URLs +const val PARTNER_BASE_URL = + "https://partner.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}/${BuildConfig.API_VERSION}" +const val PARTNER_ACCOUNT_URL = "$PARTNER_BASE_URL/accounts" + +// Connection check +const val CONN_CHECK_URL = "https://am.i.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}/json" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt new file mode 100644 index 0000000000..bbd59eba9b --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt @@ -0,0 +1,43 @@ +package net.mullvad.mullvadvpn.test.e2e.misc + +import androidx.test.platform.app.InstrumentationRegistry +import net.mullvad.mullvadvpn.test.e2e.constant.INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_AUTH +import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument +import org.junit.jupiter.api.extension.BeforeEachCallback +import org.junit.jupiter.api.extension.ExtensionContext + +class AccountTestRule : BeforeEachCallback { + + private val partnerAccount: String? + private val client = + SimpleMullvadHttpClient(InstrumentationRegistry.getInstrumentation().targetContext) + + val validAccountNumber: String + val invalidAccountNumber: String + + init { + InstrumentationRegistry.getArguments().also { bundle -> + partnerAccount = bundle.getString(PARTNER_AUTH) + + if (partnerAccount != null) { + validAccountNumber = client.createAccount() + client.addTimeToAccountUsingPartnerAuth( + accountNumber = validAccountNumber, + daysToAdd = 1, + partnerAuth = partnerAccount + ) + } else { + validAccountNumber = + bundle.getRequiredArgument(VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY) + client.removeAllDevices(validAccountNumber) + } + + invalidAccountNumber = + bundle.getRequiredArgument(INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY) + } + } + + override fun beforeEach(context: ExtensionContext) {} +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt index b97fb28f45..dff31b6049 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt @@ -9,9 +9,12 @@ import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.RequestFuture import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley -import net.mullvad.mullvadvpn.test.e2e.BuildConfig +import net.mullvad.mullvadvpn.test.e2e.constant.ACCOUNT_URL +import net.mullvad.mullvadvpn.test.e2e.constant.AUTH_URL import net.mullvad.mullvadvpn.test.e2e.constant.CONN_CHECK_URL +import net.mullvad.mullvadvpn.test.e2e.constant.DEVICE_LIST_URL import net.mullvad.mullvadvpn.test.e2e.constant.LOG_TAG +import net.mullvad.mullvadvpn.test.e2e.constant.PARTNER_ACCOUNT_URL import org.json.JSONArray import org.json.JSONObject @@ -37,6 +40,24 @@ class SimpleMullvadHttpClient(context: Context) { } } + fun createAccount(): String { + return sendSimpleSynchronousRequest(method = Request.Method.POST, url = ACCOUNT_URL)!! + .getString("number") + } + + fun addTimeToAccountUsingPartnerAuth( + accountNumber: String, + daysToAdd: Int, + partnerAuth: String + ) { + sendSimpleSynchronousRequest( + method = Request.Method.POST, + url = "$PARTNER_ACCOUNT_URL/$accountNumber/extend", + body = JSONObject().apply { put("days", "$daysToAdd") }, + authorizationHeader = "Basic $partnerAuth" + ) + } + fun getDeviceList(accessToken: String): List<String> { Log.v(LOG_TAG, "Get devices") @@ -62,9 +83,9 @@ class SimpleMullvadHttpClient(context: Context) { fun removeDevice(token: String, deviceId: String) { Log.v(LOG_TAG, "Remove device: $deviceId") sendSimpleSynchronousRequestString( - Request.Method.DELETE, - "$DEVICE_LIST_URL/$deviceId", - token = token + method = Request.Method.DELETE, + url = "$DEVICE_LIST_URL/$deviceId", + authorizationHeader = "Bearer $token" ) } @@ -83,7 +104,7 @@ class SimpleMullvadHttpClient(context: Context) { method: Int, url: String, body: JSONObject? = null, - token: String? = null + authorizationHeader: String? = null ): JSONObject? { val future = RequestFuture.newFuture<JSONObject>() val request = @@ -93,8 +114,8 @@ class SimpleMullvadHttpClient(context: Context) { if (body != null) { headers.put("Content-Type", "application/json") } - if (token != null) { - headers.put("Authorization", "Bearer $token") + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader) } return headers } @@ -114,7 +135,7 @@ class SimpleMullvadHttpClient(context: Context) { method: Int, url: String, body: String? = null, - token: String? = null + authorizationHeader: String? = null ): String? { val future = RequestFuture.newFuture<String>() val request = @@ -124,8 +145,8 @@ class SimpleMullvadHttpClient(context: Context) { if (body != null) { headers.put("Content-Type", "application/json") } - if (token != null) { - headers.put("Authorization", "Bearer $token") + if (authorizationHeader != null) { + headers.put("Authorization", authorizationHeader) } return headers } @@ -172,10 +193,6 @@ class SimpleMullvadHttpClient(context: Context) { (0 until this.length()).asSequence().map { this.get(it) as T }.iterator() companion object { - private const val AUTH_URL = - "${BuildConfig.API_BASE_URL}/auth/${BuildConfig.API_VERSION}/token" - private const val DEVICE_LIST_URL = - "${BuildConfig.API_BASE_URL}/accounts/${BuildConfig.API_VERSION}/devices" private const val REQUEST_ERROR_MESSAGE = "Unable to verify account due to invalid account or connectivity issues." } diff --git a/android/test/firebase/e2e-play-stagemole.yml b/android/test/firebase/e2e-play-stagemole.yml new file mode 100644 index 0000000000..1fecfefe94 --- /dev/null +++ b/android/test/firebase/e2e-play-stagemole.yml @@ -0,0 +1,11 @@ +--- +default: + type: instrumentation + app: android/app/build/outputs/apk/playStagemole/debug/app-play-stagemole-debug.apk + test: android/test/e2e/build/outputs/apk/playStagemole/debug/e2e-play-stagemole-debug.apk + timeout: 10m + use-orchestrator: true + device: + - {model: shiba, version: 34, locale: en, orientation: portrait} + - {model: tangorpro, version: 33, locale: en, orientation: portrait} + - {model: felix, version: 33, locale: en, orientation: portrait} diff --git a/android/test/firebase-test-lab.yml b/android/test/firebase/mockapi-oss.yml index d84e64134b..a1245f1d4f 100644 --- a/android/test/firebase-test-lab.yml +++ b/android/test/firebase/mockapi-oss.yml @@ -2,7 +2,7 @@ default: type: instrumentation app: android/app/build/outputs/apk/ossProd/debug/app-oss-prod-debug.apk - test: android/test/mockapi/build/outputs/apk/debug/mockapi-debug.apk + test: android/test/mockapi/build/outputs/apk/oss/debug/mockapi-oss-debug.apk timeout: 10m use-orchestrator: true device: diff --git a/android/test/mockapi/build.gradle.kts b/android/test/mockapi/build.gradle.kts index 3fea2d5d60..5c88d90d82 100644 --- a/android/test/mockapi/build.gradle.kts +++ b/android/test/mockapi/build.gradle.kts @@ -26,6 +26,13 @@ android { ) } + flavorDimensions += FlavorDimensions.BILLING + + productFlavors { + create(Flavors.OSS) { dimension = FlavorDimensions.BILLING } + create(Flavors.PLAY) { dimension = FlavorDimensions.BILLING } + } + testOptions { execution = "ANDROIDX_TEST_ORCHESTRATOR" } compileOptions { diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiTest.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiTest.kt index 9ee4f52e04..02e53a09d9 100644 --- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiTest.kt +++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/MockApiTest.kt @@ -12,6 +12,7 @@ import net.mullvad.mullvadvpn.lib.endpoint.CustomApiEndpointConfiguration import net.mullvad.mullvadvpn.test.common.interactor.AppInteractor import net.mullvad.mullvadvpn.test.common.rule.CaptureScreenshotOnFailedTestRule import net.mullvad.mullvadvpn.test.mockapi.constant.LOG_TAG +import net.mullvad.mullvadvpn.test.mockapi.constant.PACKAGE_NAME import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -39,7 +40,7 @@ abstract class MockApiTest { device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) targetContext = InstrumentationRegistry.getInstrumentation().targetContext - app = AppInteractor(device, targetContext) + app = AppInteractor(device, targetContext, PACKAGE_NAME) mockWebServer.start() Log.d(LOG_TAG, "Mocked web server started using port: ${mockWebServer.port}") diff --git a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt index 16123a8e1a..37e782af11 100644 --- a/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt +++ b/android/test/mockapi/src/main/kotlin/net/mullvad/mullvadvpn/test/mockapi/constant/Constants.kt @@ -1,5 +1,7 @@ package net.mullvad.mullvadvpn.test.mockapi.constant +const val PACKAGE_NAME = "net.mullvad.mullvadvpn" + const val LOG_TAG = "mullvad-mockapi" const val AUTH_TOKEN_URL_PATH = "/auth/v1/token" |
