diff options
Diffstat (limited to 'android')
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." + } +} |
