summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
Diffstat (limited to 'android')
-rw-r--r--android/buildSrc/src/main/kotlin/Dependencies.kt1
-rw-r--r--android/buildSrc/src/main/kotlin/Versions.kt1
-rw-r--r--android/e2e/build.gradle.kts39
-rw-r--r--android/e2e/e2e.properties2
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/EndToEndTest.kt14
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/constant/Constants.kt2
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/AppInteractor.kt4
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/MullvadAccountInteractor.kt12
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/CleanupAccountTestRule.kt21
-rw-r--r--android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/SimpleMullvadHttpClient.kt191
10 files changed, 284 insertions, 3 deletions
diff --git a/android/buildSrc/src/main/kotlin/Dependencies.kt b/android/buildSrc/src/main/kotlin/Dependencies.kt
index 9e9cfefbfb..18cae6a638 100644
--- a/android/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/android/buildSrc/src/main/kotlin/Dependencies.kt
@@ -1,5 +1,6 @@
object Dependencies {
const val androidMaterial = "com.google.android.material:material:${Versions.Android.material}"
+ const val androidVolley = "com.android.volley:volley:${Versions.Android.volley}"
const val commonsValidator = "commons-validator:commons-validator:${Versions.commonsValidator}"
const val jodaTime = "joda-time:joda-time:${Versions.jodaTime}"
const val junit = "junit:junit:${Versions.junit}"
diff --git a/android/buildSrc/src/main/kotlin/Versions.kt b/android/buildSrc/src/main/kotlin/Versions.kt
index d5c3563202..ec6e9ddfa1 100644
--- a/android/buildSrc/src/main/kotlin/Versions.kt
+++ b/android/buildSrc/src/main/kotlin/Versions.kt
@@ -15,6 +15,7 @@ object Versions {
const val material = "1.4.0"
const val minSdkVersion = 26
const val targetSdkVersion = 30
+ const val volley = "1.2.1"
}
object AndroidX {
diff --git a/android/e2e/build.gradle.kts b/android/e2e/build.gradle.kts
index b938203f20..232e72386c 100644
--- a/android/e2e/build.gradle.kts
+++ b/android/e2e/build.gradle.kts
@@ -1,3 +1,6 @@
+import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import java.util.Properties
+
plugins {
id(Dependencies.Plugin.androidTestId)
id(Dependencies.Plugin.kotlinAndroidId)
@@ -11,6 +14,37 @@ android {
targetSdkVersion(Versions.Android.targetSdkVersion)
testApplicationId = "net.mullvad.mullvadvpn.e2e"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ targetProjectPath = ":app"
+
+ fun Properties.addRequiredPropertyAsBuildConfigField(name: String) {
+ val value = getProperty(name) ?: throw GradleException("Missing property: $name")
+ buildConfigField(
+ type = "String",
+ name = name,
+ value = "\"$value\""
+ )
+ }
+
+ Properties().apply {
+ load(project.file("e2e.properties").inputStream())
+ addRequiredPropertyAsBuildConfigField("API_BASE_URL")
+ addRequiredPropertyAsBuildConfigField("API_VERSION")
+ }
+
+ fun MutableMap<String, String>.addOptionalPropertyAsArgument(name: String) {
+ val value = rootProject.properties.getOrDefault(name, null) as? String
+ ?: gradleLocalProperties(rootProject.projectDir).getProperty(name)
+
+ if (value != null) {
+ put(name, value)
+ }
+ }
+
+ testInstrumentationRunnerArguments += mutableMapOf<String, String>().apply {
+ put("clearPackageData", "true")
+ addOptionalPropertyAsArgument("valid_test_account_token")
+ addOptionalPropertyAsArgument("invalid_test_account_token")
+ }
}
testOptions {
@@ -22,7 +56,9 @@ android {
targetCompatibility = JavaVersion.VERSION_1_8
}
- targetProjectPath = ":app"
+ kotlinOptions {
+ jvmTarget = Versions.jvmTarget
+ }
}
val localScreenshotPath = "$buildDir/reports/androidTests/connected/screenshots"
@@ -64,5 +100,6 @@ dependencies {
implementation(Dependencies.AndroidX.testRunner)
implementation(Dependencies.AndroidX.testRules)
implementation(Dependencies.AndroidX.testUiAutomator)
+ implementation(Dependencies.androidVolley)
implementation(Dependencies.Kotlin.stdlib)
}
diff --git a/android/e2e/e2e.properties b/android/e2e/e2e.properties
new file mode 100644
index 0000000000..b02f6a9381
--- /dev/null
+++ b/android/e2e/e2e.properties
@@ -0,0 +1,2 @@
+API_BASE_URL=https://api.mullvad.net
+API_VERSION=v1-beta1
diff --git a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/EndToEndTest.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/EndToEndTest.kt
index 0ff6e0b1ae..0963821785 100644
--- a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/EndToEndTest.kt
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/EndToEndTest.kt
@@ -4,6 +4,9 @@ import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import androidx.test.uiautomator.UiDevice
+import net.mullvad.mullvadvpn.e2e.constant.INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY
+import net.mullvad.mullvadvpn.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY
+import net.mullvad.mullvadvpn.e2e.extension.getRequiredArgument
import net.mullvad.mullvadvpn.e2e.interactor.AppInteractor
import net.mullvad.mullvadvpn.e2e.misc.CaptureScreenshotOnFailedTestRule
import org.junit.Before
@@ -20,15 +23,24 @@ abstract class EndToEndTest {
lateinit var device: UiDevice
lateinit var targetContext: Context
lateinit var app: AppInteractor
+ lateinit var validTestAccountToken: String
+ lateinit var invalidTestAccountToken: String
@Before
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)
+
app = AppInteractor(
device,
- targetContext
+ targetContext,
+ validTestAccountToken,
+ invalidTestAccountToken
)
}
}
diff --git a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/constant/Constants.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/constant/Constants.kt
index 44b8029799..ed2d8f5ba2 100644
--- a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/constant/Constants.kt
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/constant/Constants.kt
@@ -1,3 +1,5 @@
package net.mullvad.mullvadvpn.e2e.constant
const val LOG_TAG = "mullvad-e2e"
+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/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/AppInteractor.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/AppInteractor.kt
index 8270f27c9e..69af7f1952 100644
--- a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/AppInteractor.kt
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/AppInteractor.kt
@@ -12,7 +12,9 @@ import net.mullvad.mullvadvpn.e2e.extension.findObjectWithTimeout
class AppInteractor(
private val device: UiDevice,
- private val targetContext: Context
+ private val targetContext: Context,
+ private val validTestAccountToken: String,
+ private val invalidTestAccountToken: String
) {
fun launch() {
device.pressHome()
diff --git a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/MullvadAccountInteractor.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/MullvadAccountInteractor.kt
new file mode 100644
index 0000000000..08c5a698c3
--- /dev/null
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/interactor/MullvadAccountInteractor.kt
@@ -0,0 +1,12 @@
+package net.mullvad.mullvadvpn.e2e.interactor
+
+import net.mullvad.mullvadvpn.e2e.misc.SimpleMullvadHttpClient
+
+class MullvadAccountInteractor(
+ private val httpClient: SimpleMullvadHttpClient,
+ private val testAccountToken: String
+) {
+ fun cleanupAccount() {
+ httpClient.removeAllDevices(testAccountToken)
+ }
+}
diff --git a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/CleanupAccountTestRule.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/CleanupAccountTestRule.kt
new file mode 100644
index 0000000000..17f7f86f6c
--- /dev/null
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/CleanupAccountTestRule.kt
@@ -0,0 +1,21 @@
+package net.mullvad.mullvadvpn.e2e.misc
+
+import android.util.Log
+import androidx.test.platform.app.InstrumentationRegistry
+import net.mullvad.mullvadvpn.e2e.constant.LOG_TAG
+import net.mullvad.mullvadvpn.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY
+import net.mullvad.mullvadvpn.e2e.extension.getRequiredArgument
+import net.mullvad.mullvadvpn.e2e.interactor.MullvadAccountInteractor
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+
+class CleanupAccountTestRule : TestWatcher() {
+ override fun starting(description: Description?) {
+ Log.d(LOG_TAG, "Cleaning up account before test: ${description?.methodName}")
+ val targetContext = InstrumentationRegistry.getInstrumentation().targetContext
+ val validTestAccountToken = InstrumentationRegistry.getArguments()
+ .getRequiredArgument(VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY)
+ MullvadAccountInteractor(SimpleMullvadHttpClient(targetContext), validTestAccountToken)
+ .cleanupAccount()
+ }
+}
diff --git a/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/SimpleMullvadHttpClient.kt b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/SimpleMullvadHttpClient.kt
new file mode 100644
index 0000000000..ebd90f1bac
--- /dev/null
+++ b/android/e2e/src/main/java/net/mullvad/mullvadvpn/e2e/misc/SimpleMullvadHttpClient.kt
@@ -0,0 +1,191 @@
+package net.mullvad.mullvadvpn.e2e.misc
+
+import android.content.Context
+import android.util.Log
+import androidx.test.services.events.TestEventException
+import com.android.volley.Request
+import com.android.volley.toolbox.JsonArrayRequest
+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.e2e.BuildConfig
+import net.mullvad.mullvadvpn.e2e.constant.LOG_TAG
+import org.json.JSONArray
+import org.json.JSONObject
+
+class SimpleMullvadHttpClient(context: Context) {
+
+ private val queue = Volley.newRequestQueue(context)
+
+ fun removeAllDevices(accountToken: String) {
+ Log.v(LOG_TAG, "Remove all devices")
+ val token = login(accountToken)
+ val devices = getDeviceList(token)
+ devices.forEach {
+ removeDevice(token, it)
+ }
+ Log.v(LOG_TAG, "All devices removed")
+ }
+
+ fun login(accountToken: String): String {
+ Log.v(LOG_TAG, "Attempt login with account token: $accountToken")
+ val json = JSONObject().apply {
+ put("account_number", accountToken)
+ }
+ return sendSimpleSynchronousRequest(Request.Method.POST, AUTH_URL, json)!!.let { response ->
+ response.getString("access_token").also { accessToken ->
+ Log.v(LOG_TAG, "Successfully logged in and received access token: $accessToken")
+ }
+ }
+ }
+
+ fun getDeviceList(accessToken: String): List<String> {
+ Log.v(LOG_TAG, "Get devices")
+
+ val response = sendSimpleSynchronousRequestArray(
+ Request.Method.GET,
+ DEVICE_LIST_URL,
+ token = accessToken
+ )
+
+ return response!!.iterator<JSONObject>().asSequence().toList()
+ .also {
+ it
+ .map { jsonObject ->
+ jsonObject.getString("name")
+ }
+ .also { deviceNames ->
+ Log.v(LOG_TAG, "Devices received: $deviceNames")
+ }
+ }
+ .map { it.getString("id") }
+ .toList()
+ }
+
+ fun removeDevice(token: String, deviceId: String) {
+ Log.v(LOG_TAG, "Remove device: $deviceId")
+ sendSimpleSynchronousRequestString(
+ Request.Method.DELETE,
+ "$DEVICE_LIST_URL/$deviceId",
+ token = token
+ )
+ }
+
+ private fun sendSimpleSynchronousRequest(
+ method: Int,
+ url: String,
+ body: JSONObject? = null,
+ token: String? = null
+ ): JSONObject? {
+ val future = RequestFuture.newFuture<JSONObject>()
+ val request = object : JsonObjectRequest(
+ method,
+ url,
+ body,
+ future,
+ future
+ ) {
+ override fun getHeaders(): MutableMap<String, String> {
+ val headers = HashMap<String, String>()
+ if (body != null) {
+ headers.put("Content-Type", "application/json")
+ }
+ if (token != null) {
+ headers.put("Authorization", "Bearer $token")
+ }
+ return headers
+ }
+ }
+ queue.add(request)
+ return try {
+ future.get().also { response ->
+ Log.v(LOG_TAG, "Json object request response: $response")
+ }
+ } catch (e: Exception) {
+ Log.v(LOG_TAG, "Json object request error: ${e.message}")
+ throw TestEventException(REQUEST_ERROR_MESSAGE)
+ }
+ }
+
+ private fun sendSimpleSynchronousRequestString(
+ method: Int,
+ url: String,
+ body: String? = null,
+ token: String? = null
+ ): String? {
+ val future = RequestFuture.newFuture<String>()
+ val request = object : StringRequest(
+ method,
+ url,
+ future,
+ future
+ ) {
+ override fun getHeaders(): MutableMap<String, String> {
+ val headers = HashMap<String, String>()
+ if (body != null) {
+ headers.put("Content-Type", "application/json")
+ }
+ if (token != null) {
+ headers.put("Authorization", "Bearer $token")
+ }
+ return headers
+ }
+ }
+ queue.add(request)
+ return try {
+ future.get().also { response ->
+ Log.v(LOG_TAG, "String request response: $response")
+ }
+ } catch (e: Exception) {
+ Log.v(LOG_TAG, "String request error: ${e.message}")
+ throw TestEventException(REQUEST_ERROR_MESSAGE)
+ }
+ }
+
+ private fun sendSimpleSynchronousRequestArray(
+ method: Int,
+ url: String,
+ body: JSONArray? = null,
+ token: String? = null
+ ): JSONArray? {
+ val future = RequestFuture.newFuture<JSONArray>()
+ val request = object : JsonArrayRequest(
+ method,
+ url,
+ null,
+ future,
+ future
+ ) {
+ override fun getHeaders(): MutableMap<String, String> {
+ val headers = HashMap<String, String>()
+ headers.put("Content-Type", "application/json")
+ if (token != null) {
+ headers.put("Authorization", "Bearer $token")
+ }
+ return headers
+ }
+ }
+ queue.add(request)
+ return try {
+ future.get().also { response ->
+ Log.v(LOG_TAG, "Json array request response: $response")
+ }
+ } catch (e: Exception) {
+ Log.v(LOG_TAG, "Json array request error: ${e.message}")
+ throw TestEventException(REQUEST_ERROR_MESSAGE)
+ }
+ }
+
+ operator fun <T> JSONArray.iterator(): Iterator<T> =
+ (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."
+ }
+}