diff options
| author | Albin <albin@mullvad.net> | 2022-03-11 11:00:12 +0100 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2022-04-20 17:52:30 +0200 |
| commit | 19cacfbd013fed75af0408492999fc031d2330d4 (patch) | |
| tree | 55977cd9738df49b5386f74fc6be82f391de9b6b /android/e2e/src/main/java | |
| parent | 339038cca27828cc060d23f846e3ab6026889af2 (diff) | |
| download | mullvadvpn-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/main/java')
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." + } +} |
