summaryrefslogtreecommitdiffhomepage
path: root/android/e2e/src
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2022-03-11 11:00:12 +0100
committerAlbin <albin@mullvad.net>2022-04-20 17:52:30 +0200
commit19cacfbd013fed75af0408492999fc031d2330d4 (patch)
tree55977cd9738df49b5386f74fc6be82f391de9b6b /android/e2e/src
parent339038cca27828cc060d23f846e3ab6026889af2 (diff)
downloadmullvadvpn-19cacfbd013fed75af0408492999fc031d2330d4.tar.xz
mullvadvpn-19cacfbd013fed75af0408492999fc031d2330d4.zip
Add mechanism to reset an account before e2e tests
Adds a mechanism to remove all keys/devices of a specified account in order to start all tests with a known state. This is achieved using a simple http client included in the e2e test apk, which support login, listing devices and removing devices. Two account used for testing _must_ be specified either as gradle properties or as runtime arguments to the test runner: * Local properties (local.properties): valid_test_account_token=XXXX invalid_test_account_token=XXXX * Gradle/CLI arguments: ./gradlew :e2e:assembleDebug -Pvalid_test_account_token=XXXX -Pinvalid_test_account_token=XXXX * Runtime arguments: am instrument -e valid_test_account_token XXXX -e invalid_test_account_token XXXX # ...
Diffstat (limited to 'android/e2e/src')
-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
6 files changed, 242 insertions, 2 deletions
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."
+ }
+}