summaryrefslogtreecommitdiffhomepage
path: root/android/test
diff options
context:
space:
mode:
Diffstat (limited to 'android/test')
-rw-r--r--android/test/benchmark/README.md20
-rw-r--r--android/test/benchmark/build.gradle.kts129
-rwxr-xr-xandroid/test/benchmark/lib/arm64-v8a/iperf3.18bin0 -> 124160 bytes
-rwxr-xr-xandroid/test/benchmark/lib/armeabi-v7a/iperf3.18bin0 -> 90972 bytes
-rwxr-xr-xandroid/test/benchmark/lib/riscv64/iperf3.18bin0 -> 112408 bytes
-rwxr-xr-xandroid/test/benchmark/lib/x86/iperf3.18bin0 -> 132836 bytes
-rwxr-xr-xandroid/test/benchmark/lib/x86_64/iperf3.18bin0 -> 129544 bytes
-rw-r--r--android/test/benchmark/libs/iperf3.jarbin0 -> 277217 bytes
-rw-r--r--android/test/benchmark/src/main/AndroidManifest.xml11
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/BenchmarkTest.kt43
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IPerf.kt31
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IperfResultTest.kt20
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/SpeedTest.kt118
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnCheckResult.kt15
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApi.kt48
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApiTest.kt19
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApi.kt120
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApiTest.kt28
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApi.kt72
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApiTest.kt26
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/constant/Constants.kt37
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/model/IperfResult.kt217
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountProvider.kt30
-rw-r--r--android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountTestRule.kt15
24 files changed, 999 insertions, 0 deletions
diff --git a/android/test/benchmark/README.md b/android/test/benchmark/README.md
new file mode 100644
index 0000000000..750e27da45
--- /dev/null
+++ b/android/test/benchmark/README.md
@@ -0,0 +1,20 @@
+# Benchmarks
+
+## Setup
+
+Setup the e2e & benchmark properties in your local `gradle.properties`.
+
+Don't forget to specify the IP and port for your target iPerf testing server.
+`mullvad.test.benchmark.target.ip=<IP>`
+`mullvad.test.benchmark.target.port=<PORT>`
+
+## Run tests
+
+Start the test server and is reachable:
+```
+iperf3 -s -p <PORT>
+```
+Make sure the ip and port matches what you've previously configured.
+
+Then start the tests by running the SpeedTests class. The output will be printed in the logs.
+
diff --git a/android/test/benchmark/build.gradle.kts b/android/test/benchmark/build.gradle.kts
new file mode 100644
index 0000000000..4eff073fbf
--- /dev/null
+++ b/android/test/benchmark/build.gradle.kts
@@ -0,0 +1,129 @@
+plugins {
+ alias(libs.plugins.android.test)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlinx.serialization)
+ alias(libs.plugins.junit5.android)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.test.benchmark"
+ compileSdk = libs.versions.compile.sdk.get().toInt()
+ buildToolsVersion = libs.versions.build.tools.get()
+
+ defaultConfig {
+ minSdk = libs.versions.min.sdk.get().toInt()
+ testApplicationId = "net.mullvad.mullvadvpn.test.benchmark"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ testInstrumentationRunnerArguments["runnerBuilder"] =
+ "de.mannodermaus.junit5.AndroidJUnit5Builder"
+ targetProjectPath = ":app"
+
+ missingDimensionStrategy(FlavorDimensions.BILLING, Flavors.PLAY)
+
+ testInstrumentationRunnerArguments += buildMap {
+ put("clearPackageData", "true")
+
+ // Add all properties starting with "test.e2e" to the testInstrumentationRunnerArguments
+ properties.forEach {
+ if (it.key.startsWith("mullvad.test.e2e") || it.key.startsWith("mullvad.test.benchmark")) {
+ put(it.key, it.value.toString())
+ }
+ }
+ }
+ }
+
+ flavorDimensions += FlavorDimensions.INFRASTRUCTURE
+
+ productFlavors {
+ 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" }
+
+ buildFeatures { buildConfig = true }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = libs.versions.jvm.target.get()
+ allWarningsAsErrors = true
+ }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+
+ packaging {
+ resources {
+ pickFirsts +=
+ setOf(
+ // Fixes packaging error caused by: jetified-junit-*
+ "META-INF/LICENSE.md",
+ "META-INF/LICENSE-notice.md",
+ )
+ }
+ }
+ kotlin {
+ compilerOptions {
+ freeCompilerArgs.add("-Xjvm-default=all-compatibility")
+ }
+ }
+}
+
+
+dependencies {
+ implementation(projects.lib.endpoint)
+ implementation(projects.test.common)
+ implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
+ implementation(libs.androidx.test.core)
+ // Fixes: https://github.com/android/android-test/issues/1589
+ implementation(libs.androidx.test.monitor)
+ implementation(libs.androidx.test.runner)
+ implementation(libs.androidx.test.rules)
+ implementation(libs.androidx.test.uiautomator)
+ implementation(libs.kermit)
+ implementation(libs.junit.jupiter.api)
+ implementation(libs.junit5.android.test.extensions)
+ implementation(libs.junit5.android.test.runner)
+ implementation(libs.mockkWebserver)
+
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.cio)
+ implementation(libs.ktor.client.auth)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.serialization.kotlinx.json)
+ implementation(libs.ktor.client.content.negotiation)
+ implementation(libs.ktor.client.resources)
+ implementation(libs.ktor.client.json)
+
+ androidTestUtil(libs.androidx.test.orchestrator)
+
+ // Needed or else the app crashes when launched
+ implementation(libs.junit5.android.test.compose)
+ implementation(libs.compose.material3)
+
+ // Need these for forcing later versions of dependencies
+ implementation(libs.compose.ui)
+ implementation(libs.androidx.activity.compose)
+}
diff --git a/android/test/benchmark/lib/arm64-v8a/iperf3.18 b/android/test/benchmark/lib/arm64-v8a/iperf3.18
new file mode 100755
index 0000000000..88b3d782e0
--- /dev/null
+++ b/android/test/benchmark/lib/arm64-v8a/iperf3.18
Binary files differ
diff --git a/android/test/benchmark/lib/armeabi-v7a/iperf3.18 b/android/test/benchmark/lib/armeabi-v7a/iperf3.18
new file mode 100755
index 0000000000..e89b4c24d3
--- /dev/null
+++ b/android/test/benchmark/lib/armeabi-v7a/iperf3.18
Binary files differ
diff --git a/android/test/benchmark/lib/riscv64/iperf3.18 b/android/test/benchmark/lib/riscv64/iperf3.18
new file mode 100755
index 0000000000..68f874238c
--- /dev/null
+++ b/android/test/benchmark/lib/riscv64/iperf3.18
Binary files differ
diff --git a/android/test/benchmark/lib/x86/iperf3.18 b/android/test/benchmark/lib/x86/iperf3.18
new file mode 100755
index 0000000000..e0c5e6d306
--- /dev/null
+++ b/android/test/benchmark/lib/x86/iperf3.18
Binary files differ
diff --git a/android/test/benchmark/lib/x86_64/iperf3.18 b/android/test/benchmark/lib/x86_64/iperf3.18
new file mode 100755
index 0000000000..3773b8ce6d
--- /dev/null
+++ b/android/test/benchmark/lib/x86_64/iperf3.18
Binary files differ
diff --git a/android/test/benchmark/libs/iperf3.jar b/android/test/benchmark/libs/iperf3.jar
new file mode 100644
index 0000000000..b6d174cdae
--- /dev/null
+++ b/android/test/benchmark/libs/iperf3.jar
Binary files differ
diff --git a/android/test/benchmark/src/main/AndroidManifest.xml b/android/test/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..bc5d11c5d4
--- /dev/null
+++ b/android/test/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="net.mullvad.mullvadvpn" />
+
+ <application
+ android:extractNativeLibs="true"
+ android:icon="@android:color/background_light" />
+</manifest>
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/BenchmarkTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/BenchmarkTest.kt
new file mode 100644
index 0000000000..da8c8c6c8c
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/BenchmarkTest.kt
@@ -0,0 +1,43 @@
+package net.mullvad.mullvadvpn.test.benchmark
+
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import co.touchlab.kermit.Logger
+import de.mannodermaus.junit5.extensions.GrantPermissionExtension
+import net.mullvad.mullvadvpn.test.benchmark.constant.LOG_TAG
+import net.mullvad.mullvadvpn.test.common.interactor.AppInteractor
+import net.mullvad.mullvadvpn.test.common.rule.CaptureScreenshotOnFailedTestRule
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.extension.RegisterExtension
+
+abstract class BenchmarkTest {
+
+ @RegisterExtension @JvmField val rule = CaptureScreenshotOnFailedTestRule(LOG_TAG)
+
+ @RegisterExtension
+ @JvmField
+ val permissionRule: GrantPermissionExtension =
+ GrantPermissionExtension.grant(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)
+
+ lateinit var device: UiDevice
+ lateinit var context: Context
+ lateinit var targetContext: Context
+ lateinit var app: AppInteractor
+
+ @BeforeEach
+ open fun setup() {
+ Logger.setTag(LOG_TAG)
+
+ device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ context = InstrumentationRegistry.getInstrumentation().context
+ targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+
+ app = AppInteractor(device, targetContext)
+ }
+
+ @AfterEach open fun teardown() {}
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IPerf.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IPerf.kt
new file mode 100644
index 0000000000..cf93b8c3af
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IPerf.kt
@@ -0,0 +1,31 @@
+package net.mullvad.mullvadvpn.test.benchmark
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import net.mullvad.mullvadvpn.test.benchmark.constant.getTargetIp
+import net.mullvad.mullvadvpn.test.benchmark.constant.getTargetPort
+import net.mullvad.mullvadvpn.test.benchmark.model.IperfResult
+
+@OptIn(ExperimentalSerializationApi::class)
+fun runIperf3(context: Context): IperfResult {
+ val iperf = File(context.applicationInfo.nativeLibraryDir + "/iperf3.18")
+ iperf.setExecutable(true)
+ val process =
+ ProcessBuilder(iperf.absolutePath, "-c", InstrumentationRegistry.getArguments().getTargetIp(), "-p",
+ InstrumentationRegistry.getArguments().getTargetPort(), "--json").start()
+ return Json.decodeFromStream<IperfResult>(process.inputStream)
+}
+
+fun IperfResult.summarize(): String = buildString {
+ appendLine("# Speed #")
+ appendLine("Receive: ${end.sumReceived.bitsPerSecond / 1000000} Mbit/s")
+ appendLine("Send: ${end.sumSent.bitsPerSecond / 1000000} Mbit/s")
+ appendLine("# CPU #")
+ appendLine("User: ${end.cpuUtilizationPercent.hostUser}")
+ appendLine("System: ${end.cpuUtilizationPercent.hostSystem}")
+ appendLine("Total: ${end.cpuUtilizationPercent.hostTotal}")
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IperfResultTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IperfResultTest.kt
new file mode 100644
index 0000000000..ab2f76cb7f
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/IperfResultTest.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.test.benchmark
+
+import co.touchlab.kermit.Logger
+import java.io.BufferedReader
+import java.io.File
+import java.io.IOException
+import java.io.InputStreamReader
+import kotlinx.serialization.json.Json
+import net.mullvad.mullvadvpn.test.benchmark.model.IperfResult
+import org.junit.jupiter.api.Test
+
+class IperfResultTest : BenchmarkTest() {
+
+ @Test
+ fun testIPerf() {
+ val result = runIperf3(context)
+ Logger.d("!!! TESTING !!!")
+ Logger.d(result.summarize())
+ }
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/SpeedTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/SpeedTest.kt
new file mode 100644
index 0000000000..7e9a67bea2
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/SpeedTest.kt
@@ -0,0 +1,118 @@
+package net.mullvad.mullvadvpn.test.benchmark
+
+import co.touchlab.kermit.Logger
+import net.mullvad.mullvadvpn.test.benchmark.rule.AccountTestRule
+import net.mullvad.mullvadvpn.test.common.extension.acceptVpnPermissionDialog
+import net.mullvad.mullvadvpn.test.common.page.ConnectPage
+import net.mullvad.mullvadvpn.test.common.page.SelectLocationPage
+import net.mullvad.mullvadvpn.test.common.page.SettingsPage
+import net.mullvad.mullvadvpn.test.common.page.VpnSettingsPage
+import net.mullvad.mullvadvpn.test.common.page.on
+import net.mullvad.mullvadvpn.test.common.rule.ForgetAllVpnAppsInSettingsTestRule
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+
+class SpeedTest : BenchmarkTest() {
+
+ @RegisterExtension @JvmField val accountTestRule = AccountTestRule()
+
+ @RegisterExtension
+ @JvmField
+ val forgetAllVpnAppsInSettingsTestRule = ForgetAllVpnAppsInSettingsTestRule()
+
+ @Test
+ fun noVpn() {
+ val result = runIperf3(context)
+
+ Logger.d("# No VPN result #")
+ Logger.d(result.summarize())
+ }
+
+ @Test
+ fun testNoObfuscation() {
+ // Given
+ app.launchAndLogIn(accountTestRule.validAccountNumber)
+
+ on<ConnectPage> { clickSettings() }
+ on<SettingsPage> { clickVpnSettings() }
+ on<VpnSettingsPage> {
+ scrollUntilWireGuardObfuscationOffCell()
+ clickWireGuardObfuscationOffCell()
+ }
+ device.pressBack()
+ device.pressBack()
+
+ on<ConnectPage> { clickSelectLocation() }
+
+ on<SelectLocationPage> {
+ clickLocationExpandButton(DEFAULT_COUNTRY)
+ clickLocationExpandButton(DEFAULT_CITY)
+ clickLocationCell(DEFAULT_RELAY)
+ }
+
+ device.acceptVpnPermissionDialog()
+
+ on<ConnectPage> { waitForConnectedLabel() }
+
+ val result = runIperf3(context)
+
+ Logger.d("# No obfuscation #")
+ Logger.d(result.summarize())
+ }
+
+ @Test
+ fun testUdpOverTcpObfuscation() {
+ // Given
+ app.launchAndLogIn(accountTestRule.validAccountNumber)
+
+ on<ConnectPage> { clickSettings() }
+ on<SettingsPage> { clickVpnSettings() }
+ on<VpnSettingsPage> {
+ scrollUntilWireGuardObfuscationUdpOverTcpCell()
+ clickWireguardObfuscationUdpOverTcpCell()
+ }
+ device.pressBack()
+ device.pressBack()
+
+ on<ConnectPage> { clickConnect() }
+
+ device.acceptVpnPermissionDialog()
+
+ on<ConnectPage> { waitForConnectedLabel() }
+
+ val result = runIperf3(context)
+
+ Logger.d("# Udp-over-Tcp #")
+ Logger.d(result.summarize())
+ }
+
+ @Test
+ fun testShadowsocks() {
+ // Given
+ app.launchAndLogIn(accountTestRule.validAccountNumber)
+
+ on<ConnectPage> { clickSettings() }
+ on<SettingsPage> { clickVpnSettings() }
+ on<VpnSettingsPage> {
+ scrollUntilWireGuardObfuscationShadowsocksCell()
+ clickWireGuardObfuscationShadowsocksCell()
+ }
+ device.pressBack()
+ device.pressBack()
+
+ on<ConnectPage> { clickConnect() }
+
+ device.acceptVpnPermissionDialog()
+
+ on<ConnectPage> { waitForConnectedLabel() }
+
+ val result = runIperf3(context)
+
+ Logger.d("# Shadowsocks #")
+ Logger.d(result.summarize())
+ }
+}
+
+const val DEFAULT_COUNTRY = "Sweden"
+const val DEFAULT_CITY = "Gothenburg"
+const val DEFAULT_RELAY = "se-got-wg-001"
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnCheckResult.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnCheckResult.kt
new file mode 100644
index 0000000000..18f5417314
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnCheckResult.kt
@@ -0,0 +1,15 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.connectioncheck
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ConnCheckResult(
+ @SerialName("mullvad_exit_ip") val mullvadExitIp: Boolean,
+ val ip: String,
+ val organization: String,
+ val country: String,
+ val city: String,
+ val longitude: Double,
+ val latitude: Double,
+)
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApi.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApi.kt
new file mode 100644
index 0000000000..a434010462
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApi.kt
@@ -0,0 +1,48 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.connectioncheck
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.request.get
+import io.ktor.http.ContentType
+import io.ktor.http.URLProtocol.Companion.HTTPS
+import io.ktor.http.contentType
+import io.ktor.http.path
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import net.mullvad.mullvadvpn.test.benchmark.BuildConfig
+
+class ConnectionCheckApi {
+ private val client: HttpClient =
+ HttpClient(CIO) {
+ install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
+ install(Logging) {
+ level = LogLevel.INFO
+ }
+ defaultRequest {
+ url {
+ protocol = HTTPS
+ host = BASE_URL
+ }
+ contentType(ContentType.Application.Json)
+ }
+ expectSuccess = true
+ }
+
+ suspend fun connectionCheck(): ConnCheckResult =
+ withContext(Dispatchers.IO) {
+ client.get { url { path(JSON_PATH) } }.body<ConnCheckResult>()
+ }
+
+ companion object {
+ // Connection check
+ private const val BASE_URL = "am.i.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}"
+ private const val JSON_PATH = "json"
+ }
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApiTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApiTest.kt
new file mode 100644
index 0000000000..39622c25c4
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/connectioncheck/ConnectionCheckApiTest.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.connectioncheck
+
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertNotNull
+
+@Disabled("Only used developing the ConnectionCheckApi")
+class ConnectionCheckApiTest {
+ private val connCheckApi = ConnectionCheckApi()
+
+ @Test
+ fun testConnCheck() = runTest {
+ val result = connCheckApi.connectionCheck()
+ assertNotNull(result)
+ assertFalse(result.mullvadExitIp)
+ }
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApi.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApi.kt
new file mode 100644
index 0000000000..6d9f75c938
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApi.kt
@@ -0,0 +1,120 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.mullvad
+
+import co.touchlab.kermit.Logger
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.plugins.resources.Resources
+import io.ktor.client.request.bearerAuth
+import io.ktor.client.request.delete
+import io.ktor.client.request.get
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.URLProtocol.Companion.HTTPS
+import io.ktor.http.contentType
+import io.ktor.http.path
+import io.ktor.serialization.kotlinx.json.json
+import io.ktor.utils.io.InternalAPI
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import net.mullvad.mullvadvpn.test.benchmark.BuildConfig
+
+class MullvadApi {
+ private val json = Json {
+ ignoreUnknownKeys = true
+ prettyPrint = true
+ isLenient = true
+ }
+ private val client: HttpClient =
+ HttpClient(CIO) {
+ install(ContentNegotiation) { json(json) }
+ install(Resources)
+ install(Logging) {
+ level = LogLevel.INFO
+ sanitizeHeader { header -> header == HttpHeaders.Authorization }
+ }
+
+ defaultRequest {
+ url {
+ protocol = HTTPS
+ host = BASE_URL
+ }
+ contentType(ContentType.Application.Json)
+ }
+ expectSuccess = true
+ }
+
+ @OptIn(InternalAPI::class)
+ suspend fun login(accountNumber: String): String =
+ withContext(Dispatchers.IO) {
+ val test =
+ client
+ .post {
+ url { path(AUTH_PATH) }
+ setBody(LoginRequest(accountNumber))
+ }
+ .also {
+ Logger.d("Login response: ${it}")
+ Logger.d("Login response: ${it.bodyAsText()}")
+ }
+ .bodyAsText()
+ json.decodeFromString<LoginResponse>(test).
+ also {
+ Logger.d { "Login response: $it" }
+ }.accessToken
+ }
+
+ @Serializable data class Device(val name: String, val id: String)
+
+ suspend fun getDeviceList(accessToken: String): List<String> =
+ withContext(Dispatchers.IO) {
+ client
+ .get {
+ url { path(DEVICES_PATH) }
+ bearerAuth(accessToken)
+ }
+ .body<List<Device>>()
+ .map { it.id }
+ }
+
+ suspend fun removeDevice(accessToken: String, deviceId: String) =
+ withContext(Dispatchers.IO) {
+ client.delete {
+ url { path("$DEVICES_PATH/$deviceId") }
+ bearerAuth(accessToken)
+ }
+ }
+
+ companion object {
+ private const val BASE_URL = "api-app.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}"
+ private const val AUTH_PATH = "auth/v1/token"
+ private const val DEVICES_PATH = "accounts/v1/devices"
+ }
+}
+
+@OptIn(ExperimentalSerializationApi::class)
+@Serializable
+data class LoginRequest(@SerialName("account_number") val accountNumber: String)
+
+@Serializable data class LoginResponse(@SerialName("access_token") val accessToken: String)
+
+suspend fun MullvadApi.removeAllDevices(accessToken: String) =
+ withContext(Dispatchers.IO) {
+ val token = login(accessToken)
+ val devices = getDeviceList(token)
+
+ devices.map { async { removeDevice(token, it) } }.awaitAll()
+ }
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApiTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApiTest.kt
new file mode 100644
index 0000000000..fec9e4e4a7
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/mullvad/MullvadApiTest.kt
@@ -0,0 +1,28 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.mullvad
+
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.test.benchmark.rule.AccountProvider
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertDoesNotThrow
+
+@Disabled("Only used developing the MullvadApi")
+class MullvadApiTest {
+ private val mullvadApi = MullvadApi()
+
+ @Test
+ fun testLogin() = runTest {
+ val validAccountNumber = AccountProvider.getValidAccountNumber()
+ val accessToken = assertDoesNotThrow { mullvadApi.login(validAccountNumber) }
+ assertTrue(accessToken.isNotBlank())
+ }
+
+ @Test
+ fun testGetDeviceList() = runTest {
+ val validAccountNumber = AccountProvider.getValidAccountNumber()
+ val accessToken = assertDoesNotThrow { mullvadApi.login(validAccountNumber) }
+ assertTrue(accessToken.isNotBlank())
+ assertDoesNotThrow { mullvadApi.getDeviceList(accessToken) }
+ }
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApi.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApi.kt
new file mode 100644
index 0000000000..f9210574d3
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApi.kt
@@ -0,0 +1,72 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.partner
+
+import io.ktor.client.HttpClient
+import io.ktor.client.call.body
+import io.ktor.client.engine.cio.CIO
+import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.client.plugins.defaultRequest
+import io.ktor.client.plugins.logging.LogLevel
+import io.ktor.client.plugins.logging.Logging
+import io.ktor.client.request.headers
+import io.ktor.client.request.post
+import io.ktor.client.request.setBody
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.ContentType
+import io.ktor.http.HttpHeaders
+import io.ktor.http.URLProtocol.Companion.HTTPS
+import io.ktor.http.contentType
+import io.ktor.http.path
+import io.ktor.serialization.kotlinx.json.json
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.Json
+import net.mullvad.mullvadvpn.test.benchmark.BuildConfig
+
+class PartnerApi(base64AuthCredentials: String) {
+ private val client: HttpClient =
+ HttpClient(CIO) {
+ install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) }
+
+ install(Logging) {
+ level = LogLevel.INFO
+ sanitizeHeader { header -> header == HttpHeaders.Authorization }
+ }
+
+ defaultRequest {
+ url {
+ protocol = HTTPS
+ host = BASE_URL
+ }
+ contentType(ContentType.Application.Json)
+
+ headers { append("Authorization", "Basic $base64AuthCredentials") }
+ }
+ expectSuccess = true
+ }
+
+ suspend fun createAccount(): String =
+ withContext(Dispatchers.IO) {
+ client.post { url { path(ACCOUNT_PATH) } }.body<CreateAccountResponse>().id
+ }
+
+ suspend fun addTime(accountNumber: String, daysToAdd: Int) =
+ withContext(Dispatchers.IO) {
+ val request = AddTimeRequest(daysToAdd)
+ client
+ .post {
+ url { path("$ACCOUNT_PATH/$accountNumber/extend") }
+ setBody(request)
+ }
+ .bodyAsText()
+ }
+
+ companion object {
+ private const val BASE_URL = "partner.${BuildConfig.INFRASTRUCTURE_BASE_DOMAIN}"
+ private const val ACCOUNT_PATH = "v1/accounts"
+ }
+}
+
+@Serializable data class CreateAccountResponse(val id: String)
+
+@Serializable data class AddTimeRequest(val days: Int)
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApiTest.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApiTest.kt
new file mode 100644
index 0000000000..d709a1331e
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/api/partner/PartnerApiTest.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.test.benchmark.api.partner
+
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.test.runTest
+import net.mullvad.mullvadvpn.test.benchmark.constant.getPartnerAuth
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertDoesNotThrow
+
+@Disabled("Only used developing the PartnerApi")
+class PartnerApiTest {
+ private val partnerApi = PartnerApi(InstrumentationRegistry.getArguments().getPartnerAuth()!!)
+
+ @Test
+ fun testCreateAccount() = runTest {
+ val accessToken = assertDoesNotThrow { partnerApi.createAccount() }
+ assertTrue(accessToken.isNotBlank())
+ }
+
+ @Test
+ fun testAddTime() = runTest {
+ val accessToken = assertDoesNotThrow { partnerApi.createAccount() }
+ assertDoesNotThrow { partnerApi.addTime(accessToken, 1) }
+ }
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/constant/Constants.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/constant/Constants.kt
new file mode 100644
index 0000000000..6d7859386c
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/constant/Constants.kt
@@ -0,0 +1,37 @@
+package net.mullvad.mullvadvpn.test.benchmark.constant
+
+import android.os.Bundle
+import net.mullvad.mullvadvpn.test.benchmark.BuildConfig
+
+const val LOG_TAG = "mullvad-benchmark"
+
+fun Bundle.getRequiredArgument(argument: String): String {
+ return getString(argument)
+ ?: throw IllegalArgumentException("Missing required argument: $argument")
+}
+
+fun Bundle.getPartnerAuth() = getString("mullvad.test.e2e.${BuildConfig.FLAVOR}.partnerAuth")
+
+fun Bundle.getValidAccountNumber() =
+ getRequiredArgument("mullvad.test.e2e.${BuildConfig.FLAVOR}.accountNumber.valid")
+
+fun Bundle.getTargetIp() = getRequiredArgument("mullvad.test.benchmark.target.ip")
+
+fun Bundle.getTargetPort() = getRequiredArgument("mullvad.test.benchmark.target.port")
+
+fun Bundle.getInvalidAccountNumber() =
+ getRequiredArgument("mullvad.test.e2e.${BuildConfig.FLAVOR}.accountNumber.invalid")
+
+fun Bundle.isRaasEnabled(): Boolean =
+ getRequiredArgument("mullvad.test.e2e.config.raas.enable").toBoolean()
+
+fun Bundle.isHighlyRateLimitedTestsEnabled(): Boolean =
+ getRequiredArgument("mullvad.test.e2e.config.runHighlyRateLimitedTests").toBoolean()
+
+fun Bundle.getRaasHost() = getRequiredArgument("mullvad.test.e2e.config.raas.host")
+
+fun Bundle.getTrafficGeneratorHost(): String =
+ getRequiredArgument("mullvad.test.e2e.config.raas.trafficGenerator.target.host")
+
+fun Bundle.getTrafficGeneratorPort(): Int =
+ getRequiredArgument("mullvad.test.e2e.config.raas.trafficGenerator.target.port").toInt()
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/model/IperfResult.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/model/IperfResult.kt
new file mode 100644
index 0000000000..a7d6aba7d5
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/model/IperfResult.kt
@@ -0,0 +1,217 @@
+package net.mullvad.mullvadvpn.test.benchmark.model
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class IperfResult(
+ val start: Start,
+ val intervals: List<Interval>,
+ val end: End,
+)
+
+@Serializable
+data class Start(
+ val connected: List<Connected>,
+ val version: String,
+
+ @SerialName("system_info")
+ val systemInfo: String,
+ val timestamp: Timestamp,
+ @SerialName("connecting_to")
+ val connectingTo: ConnectingTo,
+ val cookie: String,
+ @SerialName("tcp_mss_default")
+ val tcpMssDefault: Long,
+ @SerialName("target_bitrate")
+ val targetBitrate: Long,
+ @SerialName("fq_rate")
+ val fqRate: Long,
+ @SerialName("sock_bufsize")
+ val sockBufsize: Long,
+ @SerialName("sndbuf_actual")
+ val sndbufActual: Long,
+ @SerialName("rcvbuf_actual")
+ val rcvbufActual: Long,
+ @SerialName("test_start")
+ val testStart: TestStart,
+)
+
+@Serializable
+data class Connected(
+ val socket: Long,
+ @SerialName("local_host")
+ val localHost: String,
+ @SerialName("local_port")
+ val localPort: Long,
+ @SerialName("remote_host")
+ val remoteHost: String,
+ @SerialName("remote_port")
+ val remotePort: Long,
+)
+
+@Serializable
+data class Timestamp(
+ val time: String,
+ val timesecs: Long,
+)
+
+@Serializable
+data class ConnectingTo(
+ val host: String,
+ val port: Long,
+)
+
+@Serializable
+data class TestStart(
+ val protocol: String,
+ @SerialName("num_streams")
+ val numStreams: Long,
+ val blksize: Long,
+ val omit: Long,
+ val duration: Long,
+ val bytes: Long,
+ val blocks: Long,
+ val reverse: Long,
+ val tos: Long,
+ @SerialName("target_bitrate")
+ val targetBitrate: Long,
+ val bidir: Long,
+ val fqrate: Long,
+ val interval: Long,
+)
+
+@Serializable
+data class Interval(
+ val streams: List<Stream>,
+ val sum: Sum,
+)
+
+@Serializable
+data class Stream(
+ val socket: Long,
+ val start: Double,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val retransmits: Long,
+ @SerialName("snd_cwnd")
+ val sndCwnd: Long,
+ @SerialName("snd_wnd")
+ val sndWnd: Long,
+ val rtt: Long,
+ val rttvar: Long,
+ val pmtu: Long,
+ val omitted: Boolean,
+ val sender: Boolean,
+)
+
+@Serializable
+data class Sum(
+ val start: Double,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val retransmits: Long,
+ val omitted: Boolean,
+ val sender: Boolean,
+)
+
+@Serializable
+data class End(
+ val streams: List<Stream2>,
+ @SerialName("sum_sent")
+ val sumSent: SumSent,
+ @SerialName("sum_received")
+ val sumReceived: SumReceived,
+ @SerialName("cpu_utilization_percent")
+ val cpuUtilizationPercent: CpuUtilizationPercent,
+ @SerialName("sender_tcp_congestion")
+ val senderTcpCongestion: String,
+ @SerialName("receiver_tcp_congestion")
+ val receiverTcpCongestion: String,
+)
+
+@Serializable
+data class Stream2(
+ val sender: Sender,
+ val receiver: Receiver,
+)
+
+@Serializable
+data class Sender(
+ val socket: Long,
+ val start: Long,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val retransmits: Long,
+ @SerialName("max_snd_cwnd")
+ val maxSndCwnd: Long,
+ @SerialName("max_snd_wnd")
+ val maxSndWnd: Long,
+ @SerialName("max_rtt")
+ val maxRtt: Long,
+ @SerialName("min_rtt")
+ val minRtt: Long,
+ @SerialName("mean_rtt")
+ val meanRtt: Long,
+ val sender: Boolean,
+)
+
+@Serializable
+data class Receiver(
+ val socket: Long,
+ val start: Long,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val sender: Boolean,
+)
+
+@Serializable
+data class SumSent(
+ val start: Long,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val retransmits: Long,
+ val sender: Boolean,
+)
+
+@Serializable
+data class SumReceived(
+ val start: Long,
+ val end: Double,
+ val seconds: Double,
+ val bytes: Long,
+ @SerialName("bits_per_second")
+ val bitsPerSecond: Double,
+ val sender: Boolean,
+)
+
+@Serializable
+data class CpuUtilizationPercent(
+ @SerialName("host_total")
+ val hostTotal: Double,
+ @SerialName("host_user")
+ val hostUser: Double,
+ @SerialName("host_system")
+ val hostSystem: Double,
+ @SerialName("remote_total")
+ val remoteTotal: Double,
+ @SerialName("remote_user")
+ val remoteUser: Double,
+ @SerialName("remote_system")
+ val remoteSystem: Double,
+)
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountProvider.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountProvider.kt
new file mode 100644
index 0000000000..64e7a8b224
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountProvider.kt
@@ -0,0 +1,30 @@
+package net.mullvad.mullvadvpn.test.benchmark.rule
+
+import androidx.test.platform.app.InstrumentationRegistry
+import net.mullvad.mullvadvpn.test.benchmark.api.mullvad.MullvadApi
+import net.mullvad.mullvadvpn.test.benchmark.api.mullvad.removeAllDevices
+import net.mullvad.mullvadvpn.test.benchmark.api.partner.PartnerApi
+import net.mullvad.mullvadvpn.test.benchmark.constant.getInvalidAccountNumber
+import net.mullvad.mullvadvpn.test.benchmark.constant.getPartnerAuth
+import net.mullvad.mullvadvpn.test.benchmark.constant.getValidAccountNumber
+
+object AccountProvider {
+ private val mullvadClient = MullvadApi()
+ private val partnerAuth: String? = InstrumentationRegistry.getArguments().getPartnerAuth()
+ private val partnerClient: PartnerApi by lazy { PartnerApi(partnerAuth!!) }
+
+ suspend fun getValidAccountNumber() =
+ // 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)
+ accountNumber
+ } else {
+ val validAccountNumber = InstrumentationRegistry.getArguments().getValidAccountNumber()
+ mullvadClient.removeAllDevices(validAccountNumber)
+ validAccountNumber
+ }
+
+ fun getInvalidAccountNumber() = InstrumentationRegistry.getArguments().getInvalidAccountNumber()
+}
diff --git a/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountTestRule.kt b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountTestRule.kt
new file mode 100644
index 0000000000..4859b92f2e
--- /dev/null
+++ b/android/test/benchmark/src/main/kotlin/net/mullvad/mullvadvpn/test/benchmark/rule/AccountTestRule.kt
@@ -0,0 +1,15 @@
+package net.mullvad.mullvadvpn.test.benchmark.rule
+
+import kotlinx.coroutines.runBlocking
+import org.junit.jupiter.api.extension.BeforeEachCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+
+class AccountTestRule : BeforeEachCallback {
+ lateinit var validAccountNumber: String
+ lateinit var invalidAccountNumber: String
+
+ override fun beforeEach(context: ExtensionContext): Unit = runBlocking {
+ validAccountNumber = AccountProvider.getValidAccountNumber()
+ invalidAccountNumber = AccountProvider.getInvalidAccountNumber()
+ }
+}