diff options
| author | David Göransson <david.goransson@mullvad.net> | 2025-07-08 16:09:54 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2025-07-10 13:12:04 +0200 |
| commit | bcb4749950f75edd63b2200e4c15fc73479a7fb3 (patch) | |
| tree | cfaac4be58e42b2fdaed9aad223dd006c851935a | |
| parent | 396e0791d037fd939d9837ee1f2768ad5c73dc49 (diff) | |
| download | mullvadvpn-bcb4749950f75edd63b2200e4c15fc73479a7fb3.tar.xz mullvadvpn-bcb4749950f75edd63b2200e4c15fc73479a7fb3.zip | |
Add e2e test for Google play purchases
14 files changed, 179 insertions, 6 deletions
diff --git a/.github/workflows/android-app.yml b/.github/workflows/android-app.yml index 5896844b3a..5f55987711 100644 --- a/.github/workflows/android-app.yml +++ b/.github/workflows/android-app.yml @@ -487,6 +487,7 @@ jobs: ${{ needs.prepare.outputs.E2E_TEST_INFRA_FLAVOR == 'stagemole' && secrets.STAGEMOLE_PARTNER_AUTH || '' }} VALID_TEST_ACCOUNT_NUMBER: ${{ env.RESOLVED_TEST_ACCOUNT }} INVALID_TEST_ACCOUNT_NUMBER: '0000000000000000' + ENABLE_BILLING_TESTS: true ENABLE_HIGHLY_RATE_LIMITED_TESTS: ${{ github.event_name == 'schedule' && 'true' || 'false' }} ENABLE_RAAS_TESTS: true RAAS_HOST: '192.168.105.1' diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/AddTimeBottomSheet.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/AddTimeBottomSheet.kt index edb28e0e69..083a271dfd 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/AddTimeBottomSheet.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/AddTimeBottomSheet.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -53,6 +54,7 @@ import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaDisabled import net.mullvad.mullvadvpn.lib.theme.color.AlphaVisible +import net.mullvad.mullvadvpn.lib.ui.tag.ADD_TIME_BOTTOM_SHEET_TITLE_TEST_TAG import net.mullvad.mullvadvpn.util.Lc import net.mullvad.mullvadvpn.viewmodel.AddMoreTimeSideEffect import net.mullvad.mullvadvpn.viewmodel.AddTimeViewModel @@ -500,7 +502,11 @@ private fun ColumnScope.Loading(onBackgroundColor: Color, backgroundColor: Color @Composable private fun SheetTitle(title: String, onBackgroundColor: Color, backgroundColor: Color) { - HeaderCell(text = title, background = backgroundColor) + HeaderCell( + text = title, + background = backgroundColor, + modifier = Modifier.testTag(ADD_TIME_BOTTOM_SHEET_TITLE_TEST_TAG), + ) HorizontalDivider( color = onBackgroundColor, modifier = Modifier.padding(horizontal = Dimens.mediumPadding), diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadModalBottomSheet.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadModalBottomSheet.kt index 83f4758db4..c7b5e0c582 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadModalBottomSheet.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadModalBottomSheet.kt @@ -15,6 +15,8 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.tooling.preview.Preview import net.mullvad.mullvadvpn.compose.cell.HeaderCell import net.mullvad.mullvadvpn.compose.cell.IconCell @@ -53,7 +55,7 @@ fun MullvadModalBottomSheet( onDismissRequest = onDismissRequest, sheetState = sheetState, containerColor = backgroundColor, - modifier = modifier, + modifier = modifier.semantics { testTagsAsResourceId = true }, contentWindowInsets = { WindowInsets(0, 0, 0, 0) }, // No insets dragHandle = { BottomSheetDefaults.DragHandle(color = onBackgroundColor) }, ) { diff --git a/android/gradle.properties b/android/gradle.properties index 22dc06c43c..ffb8ff476b 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -52,6 +52,8 @@ mullvad.app.build.boringtun.enable=false #mullvad.test.e2e.prod.accountNumber.valid= #mullvad.test.e2e.prod.accountNumber.invalid=1234123412341234 +# Run tests that require a valid google play test account +mullvad.test.e2e.config.billing.enable=false # Run the highly rate limited tests, these will make the test run go for longer # since it will have to be careful not to trigger the rate limiting. diff --git a/android/lib/ui/tag/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/tag/TestTagConstants.kt b/android/lib/ui/tag/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/tag/TestTagConstants.kt index 8788abfc42..a8264b9b56 100644 --- a/android/lib/ui/tag/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/tag/TestTagConstants.kt +++ b/android/lib/ui/tag/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/tag/TestTagConstants.kt @@ -130,3 +130,6 @@ const val UDP_OVER_TCP_PORT_ITEM_X_TEST_TAG = "udp_over_tcp_item_%d_test_tag" const val SHADOWSOCKS_PORT_ITEM_AUTOMATIC_TEST_TAG = "shadowsocks_item_automatic_test_tag" const val SHADOWSOCKS_PORT_ITEM_X_TEST_TAG = "shadowsocks_item_%d_test_tag" const val SHADOWSOCKS_CUSTOM_PORT_TEXT_TEST_TAG = "shadowsocks_custom_port_text_test_tag" + +// AddTimeBottomSheet +const val ADD_TIME_BOTTOM_SHEET_TITLE_TEST_TAG = "add_time_bottom_sheet_title_test_tag" diff --git a/android/scripts/run-instrumented-tests.sh b/android/scripts/run-instrumented-tests.sh index 96d8dc8eb6..86035942f9 100755 --- a/android/scripts/run-instrumented-tests.sh +++ b/android/scripts/run-instrumented-tests.sh @@ -16,6 +16,7 @@ TEST_SERVICES_URL=https://dl.google.com/android/maven2/androidx/test/services/te PARTNER_AUTH="${PARTNER_AUTH:-}" VALID_TEST_ACCOUNT_NUMBER="${VALID_TEST_ACCOUNT_NUMBER:-}" INVALID_TEST_ACCOUNT_NUMBER="${INVALID_TEST_ACCOUNT_NUMBER:-}" +ENABLE_BILLING_TESTS="${ENABLE_BILLING_TESTS:-false}" ENABLE_HIGHLY_RATE_LIMITED_TESTS="${ENABLE_HIGHLY_RATE_LIMITED_TESTS:-false}" ENABLE_RAAS_TESTS="${ENABLE_RAAS_TESTS:-false}" RAAS_HOST="${RAAS_HOST:-}" @@ -136,6 +137,12 @@ case "$TEST_TYPE" in echo "Error: The variable PARTNER_AUTH or VALID_TEST_ACCOUNT_NUMBER must be set." exit 1 fi + + if [[ ${ENABLE_BILLING_TESTS} == "true" ]]; then + echo "Tests dependent on billing account enabled" + OPTIONAL_TEST_ARGUMENTS+=" -e mullvad.test.e2e.config.billing.enable $ENABLE_BILLING_TESTS" + fi + OPTIONAL_TEST_ARGUMENTS+=" -e mullvad.test.e2e.config.raas.enable $ENABLE_RAAS_TESTS" if [[ -n ${ENABLE_RAAS_TESTS} ]]; then diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/AddTimeBottomSheet.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/AddTimeBottomSheet.kt new file mode 100644 index 0000000000..ce6a29676c --- /dev/null +++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/AddTimeBottomSheet.kt @@ -0,0 +1,25 @@ +package net.mullvad.mullvadvpn.test.common.page + +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import net.mullvad.mullvadvpn.lib.ui.tag.ADD_TIME_BOTTOM_SHEET_TITLE_TEST_TAG +import net.mullvad.mullvadvpn.test.common.constant.LONG_TIMEOUT +import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout + +class AddTimeBottomSheet internal constructor() : Page() { + private val oneMonthSelector = By.textStartsWith("Add 30 days time") + + override fun assertIsDisplayed() { + uiDevice.findObjectWithTimeout(By.res(ADD_TIME_BOTTOM_SHEET_TITLE_TEST_TAG)) + } + + fun click30days() { + uiDevice.findObjectWithTimeout(oneMonthSelector).click() + } +} + +fun UiDevice.buyGooglePlayTime() { + findObjectWithTimeout(By.text("1-tap buy"), LONG_TIMEOUT) + findObjectWithTimeout(By.text("1-tap buy")).click() + waitForIdle() +} diff --git a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/OutOfTimePage.kt b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/OutOfTimePage.kt index 33e11f4a2e..f3add0e487 100644 --- a/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/OutOfTimePage.kt +++ b/android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/OutOfTimePage.kt @@ -10,4 +10,8 @@ class OutOfTimePage internal constructor() : Page() { override fun assertIsDisplayed() { uiDevice.findObjectWithTimeout(outOfTimeSelector) } + + fun clickAddTime() { + uiDevice.findObjectWithTimeout(By.text("Add time")).click() + } } diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/PaymentTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/PaymentTest.kt new file mode 100644 index 0000000000..fbf91058f5 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/PaymentTest.kt @@ -0,0 +1,49 @@ +package net.mullvad.mullvadvpn.test.e2e + +import androidx.test.uiautomator.By +import net.mullvad.mullvadvpn.lib.ui.tag.CONNECT_CARD_HEADER_TEST_TAG +import net.mullvad.mullvadvpn.test.common.annotation.SkipForFlavors +import net.mullvad.mullvadvpn.test.common.constant.VERY_LONG_TIMEOUT +import net.mullvad.mullvadvpn.test.common.extension.findObjectWithTimeout +import net.mullvad.mullvadvpn.test.common.page.AddTimeBottomSheet +import net.mullvad.mullvadvpn.test.common.page.LoginPage +import net.mullvad.mullvadvpn.test.common.page.OutOfTimePage +import net.mullvad.mullvadvpn.test.common.page.buyGooglePlayTime +import net.mullvad.mullvadvpn.test.common.page.on +import net.mullvad.mullvadvpn.test.e2e.annotations.RequiresGoogleBillingAccount +import net.mullvad.mullvadvpn.test.e2e.annotations.RequiresPartnerAuth +import net.mullvad.mullvadvpn.test.e2e.misc.AccountTestRule +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +class PaymentTest : EndToEndTest() { + + @RegisterExtension @JvmField val accountTestRule = AccountTestRule(withTime = false) + + @Test + @SkipForFlavors(currentFlavor = BuildConfig.FLAVOR_billing, "oss") + @RequiresGoogleBillingAccount + @RequiresPartnerAuth + fun testInAppPurchaseForOutOfTime() { + val validTestAccountNumber = accountTestRule.validAccountNumber + + app.launchAndEnsureOnLoginPage() + + on<LoginPage> { + enterAccountNumber(validTestAccountNumber) + clickLoginButton() + } + + on<OutOfTimePage> { clickAddTime() } + + on<AddTimeBottomSheet> { click30days() } + + device.buyGooglePlayTime() + + // Assert we reach the Connect page after purchase + device.findObjectWithTimeout( + By.res(CONNECT_CARD_HEADER_TEST_TAG), + timeout = VERY_LONG_TIMEOUT, + ) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresGoogleBillingAccount.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresGoogleBillingAccount.kt new file mode 100644 index 0000000000..7cedfd07d3 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresGoogleBillingAccount.kt @@ -0,0 +1,34 @@ +package net.mullvad.mullvadvpn.test.e2e.annotations + +import androidx.test.platform.app.InstrumentationRegistry +import net.mullvad.mullvadvpn.test.e2e.constant.isBillingEnabled +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 + +/** + * Annotation for tests making use of a google billing test account in order to perform purchases + */ +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(RequiresGoogleBillingAccount.AccessToBillingTestAccount::class) +annotation class RequiresGoogleBillingAccount { + class AccessToBillingTestAccount : ExecutionCondition { + override fun evaluateExecutionCondition( + context: ExtensionContext? + ): ConditionEvaluationResult { + + val enable = InstrumentationRegistry.getArguments().isBillingEnabled() + + return if (enable) { + ConditionEvaluationResult.enabled( + "Running test which requires access to billing test account." + ) + } else { + ConditionEvaluationResult.disabled( + "Skipping test which requires access to billing test account." + ) + } + } + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresPartnerAuth.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresPartnerAuth.kt new file mode 100644 index 0000000000..d9afdc1d68 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresPartnerAuth.kt @@ -0,0 +1,33 @@ +package net.mullvad.mullvadvpn.test.e2e.annotations + +import androidx.test.platform.app.InstrumentationRegistry +import net.mullvad.mullvadvpn.test.e2e.constant.getPartnerAuth +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 + +/** Annotation for tests requiring a partner api authentication. */ +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(RequiresPartnerAuth.HasPartnerAuth::class) +annotation class RequiresPartnerAuth { + class HasPartnerAuth : ExecutionCondition { + override fun evaluateExecutionCondition( + context: ExtensionContext? + ): ConditionEvaluationResult { + + val provided = + InstrumentationRegistry.getArguments().getPartnerAuth()?.isNotEmpty() ?: false + + return if (provided) { + ConditionEvaluationResult.enabled( + "Running test which requires partner authentication." + ) + } else { + ConditionEvaluationResult.disabled( + "Skipping test which requires partner authentication." + ) + } + } + } +} 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 8bcb5c2997..4326d156ea 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 @@ -23,6 +23,11 @@ fun Bundle.getInvalidAccountNumber() = "mullvad.test.e2e.${BuildConfig.FLAVOR_infrastructure}.accountNumber.invalid" ) +fun Bundle.isBillingEnabled(): Boolean = + InstrumentationRegistry.getArguments() + .getString("mullvad.test.e2e.config.billing.enable", "false") + .toBoolean() + fun Bundle.isRaasEnabled(): Boolean = InstrumentationRegistry.getArguments() .getRequiredArgument("mullvad.test.e2e.config.raas.enable") diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt index 799f33be64..1d967c2fe6 100644 --- a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt @@ -13,12 +13,14 @@ object AccountProvider { private val partnerAuth: String? = InstrumentationRegistry.getArguments().getPartnerAuth() private val partnerClient: PartnerApi by lazy { PartnerApi(partnerAuth!!) } - suspend fun getValidAccountNumber() = + suspend fun getValidAccountNumber(withTime: Boolean = true) = // If partner auth is provided, create a new account using the partner API. Otherwise we // expect and account with time to be provided. if (partnerAuth != null) { val accountNumber = partnerClient.createAccount() - partnerClient.addTime(accountNumber = accountNumber, daysToAdd = 1) + if (withTime) { + partnerClient.addTime(accountNumber = accountNumber, daysToAdd = 1) + } accountNumber } else { val validAccountNumber = InstrumentationRegistry.getArguments().getValidAccountNumber() 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 index 1f455af5c1..d3c8f30c19 100644 --- 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 @@ -4,12 +4,12 @@ import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.extension.BeforeEachCallback import org.junit.jupiter.api.extension.ExtensionContext -class AccountTestRule : BeforeEachCallback { +class AccountTestRule(val withTime: Boolean = true) : BeforeEachCallback { lateinit var validAccountNumber: String lateinit var invalidAccountNumber: String override fun beforeEach(context: ExtensionContext): Unit = runBlocking { - validAccountNumber = AccountProvider.getValidAccountNumber() + validAccountNumber = AccountProvider.getValidAccountNumber(withTime) invalidAccountNumber = AccountProvider.getInvalidAccountNumber() } } |
