summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2025-07-08 16:09:54 +0200
committerDavid Göransson <david.goransson@mullvad.net>2025-07-10 13:12:04 +0200
commitbcb4749950f75edd63b2200e4c15fc73479a7fb3 (patch)
treecfaac4be58e42b2fdaed9aad223dd006c851935a
parent396e0791d037fd939d9837ee1f2768ad5c73dc49 (diff)
downloadmullvadvpn-bcb4749950f75edd63b2200e4c15fc73479a7fb3.tar.xz
mullvadvpn-bcb4749950f75edd63b2200e4c15fc73479a7fb3.zip
Add e2e test for Google play purchases
-rw-r--r--.github/workflows/android-app.yml1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/AddTimeBottomSheet.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/MullvadModalBottomSheet.kt4
-rw-r--r--android/gradle.properties2
-rw-r--r--android/lib/ui/tag/src/main/kotlin/net/mullvad/mullvadvpn/lib/ui/tag/TestTagConstants.kt3
-rwxr-xr-xandroid/scripts/run-instrumented-tests.sh7
-rw-r--r--android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/AddTimeBottomSheet.kt25
-rw-r--r--android/test/common/src/main/kotlin/net/mullvad/mullvadvpn/test/common/page/OutOfTimePage.kt4
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/PaymentTest.kt49
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresGoogleBillingAccount.kt34
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/annotations/RequiresPartnerAuth.kt33
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt5
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountProvider.kt6
-rw-r--r--android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/AccountTestRule.kt4
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()
}
}