diff options
Diffstat (limited to 'android/test/e2e/src')
20 files changed, 693 insertions, 0 deletions
diff --git a/android/test/e2e/src/main/AndroidManifest.xml b/android/test/e2e/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..931f79d291 --- /dev/null +++ b/android/test/e2e/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ +<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" /> +</manifest> diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt new file mode 100644 index 0000000000..feabbc5335 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/ConnectionTest.kt @@ -0,0 +1,32 @@ +package net.mullvad.mullvadvpn.test.e2e + +import androidx.test.uiautomator.By +import junit.framework.Assert.assertEquals +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectWithTimeout +import net.mullvad.mullvadvpn.test.e2e.interactor.WebViewInteractor +import net.mullvad.mullvadvpn.test.e2e.misc.CleanupAccountTestRule +import org.junit.Rule +import org.junit.Test + +class ConnectionTest : EndToEndTest() { + + @Rule + @JvmField + val cleanupAccountTestRule = CleanupAccountTestRule() + + @Test + fun testConnectAndVerifyWithConnectionCheck() { + // Given + app.launchAndEnsureLoggedIn() + + // When + device.findObjectWithTimeout(By.text("Secure my connection")).click() + device.findObjectWithTimeout(By.text("OK")).click() + device.findObjectWithTimeout(By.text("SECURE CONNECTION")) + val expected = WebViewInteractor.ConnCheckState(true, app.extractIpAddress()) + + // Then + val result = web.launchAndExtractConnCheckState() + assertEquals(expected, result) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt new file mode 100644 index 0000000000..8adf065663 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/EndToEndTest.kt @@ -0,0 +1,53 @@ +package net.mullvad.mullvadvpn.test.e2e + +import android.content.Context +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.AndroidJUnit4 +import androidx.test.uiautomator.UiDevice +import net.mullvad.mullvadvpn.test.e2e.constant.INVALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument +import net.mullvad.mullvadvpn.test.e2e.interactor.AppInteractor +import net.mullvad.mullvadvpn.test.e2e.interactor.WebViewInteractor +import net.mullvad.mullvadvpn.test.e2e.misc.CaptureScreenshotOnFailedTestRule +import org.junit.Before +import org.junit.Rule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +abstract class EndToEndTest { + + @Rule + @JvmField + val rule = CaptureScreenshotOnFailedTestRule() + + lateinit var device: UiDevice + lateinit var targetContext: Context + lateinit var app: AppInteractor + lateinit var web: WebViewInteractor + 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, + validTestAccountToken, + invalidTestAccountToken + ) + + web = WebViewInteractor( + targetContext, + device + ) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt new file mode 100644 index 0000000000..64f534990c --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LaunchAppTest.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.test.e2e + +import androidx.test.runner.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LaunchAppTest : EndToEndTest() { + @Test + fun testLaunchApp() { + app.launch() + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt new file mode 100644 index 0000000000..d16795e5dd --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/LoginTest.kt @@ -0,0 +1,58 @@ +package net.mullvad.mullvadvpn.test.e2e + +import androidx.test.runner.AndroidJUnit4 +import androidx.test.uiautomator.By +import junit.framework.Assert.assertNotNull +import net.mullvad.mullvadvpn.test.e2e.constant.LOGIN_FAILURE_TIMEOUT +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectWithTimeout +import net.mullvad.mullvadvpn.test.e2e.misc.CleanupAccountTestRule +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LoginTest : EndToEndTest() { + + @Rule + @JvmField + val cleanupAccountTestRule = CleanupAccountTestRule() + + @Test + fun testLoginWithInvalidCredentials() { + // Given + val invalidDummyAccountToken = invalidTestAccountToken + + // When + app.launch() + app.attemptLogin(invalidDummyAccountToken) + + // Then + device.findObjectWithTimeout(By.text("Login failed"), LOGIN_FAILURE_TIMEOUT) + } + + @Test + fun testLoginWithValidCredentials() { + // Given + val token = validTestAccountToken + + // When + app.launchAndEnsureLoggedIn(token) + + // Then + app.ensureLoggedIn() + } + + @Test + fun testLogout() { + // Given + app.launchAndEnsureLoggedIn() + + // When + app.clickSettingsCog() + app.clickListItemByText("Account") + app.clickActionButtonByText("Log out") + + // Then + assertNotNull(device.findObjectWithTimeout(By.text("Login"))) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt new file mode 100644 index 0000000000..a6a04bf094 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/WebLinkTest.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.test.e2e + +import androidx.test.uiautomator.By +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectWithTimeout +import org.junit.Test + +class WebLinkTest : EndToEndTest() { + @Test + fun testOpenFaqFromApp() { + // Given + app.launch() + + // When + device.findObjectWithTimeout(By.text("Login")) + app.clickSettingsCog() + app.clickListItemByText("FAQs & Guides") + + // Then + device.findObjectWithTimeout(By.text("Mullvad help center")) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt new file mode 100644 index 0000000000..23f29fca3c --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/Constants.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +const val LOG_TAG = "mullvad-e2e" +const val CONN_CHECK_URL = "https://mullvad.net/en/check/" +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/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/PackageConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/PackageConstants.kt new file mode 100644 index 0000000000..5476c37687 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/PackageConstants.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +const val MULLVAD_PACKAGE = "net.mullvad.mullvadvpn" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ResourceConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ResourceConstants.kt new file mode 100644 index 0000000000..7b27a581ad --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/ResourceConstants.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +const val SETTINGS_COG_ID = "net.mullvad.mullvadvpn:id/settings" +const val TUNNEL_INFO_ID = "net.mullvad.mullvadvpn:id/tunnel_info" +const val TUNNEL_OUT_ADDRESS_ID = "net.mullvad.mullvadvpn:id/out_address" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TextConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TextConstants.kt new file mode 100644 index 0000000000..cfc4080ea4 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TextConstants.kt @@ -0,0 +1,4 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +const val CONNECTION_CHECK_IS_CONNECTED = "Using Mullvad VPN" +const val CONNECTION_CHECK_IS_NOT_CONNECTED = "Not using Mullvad VPN" diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TimeoutConstants.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TimeoutConstants.kt new file mode 100644 index 0000000000..6599a26bcd --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/constant/TimeoutConstants.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.test.e2e.constant + +const val APP_LAUNCH_TIMEOUT = 5000L +const val CONNECTION_TIMEOUT = 30000L +const val DEFAULT_INTERACTION_TIMEOUT = 3000L +const val LOGIN_TIMEOUT = 30000L +const val LOGIN_FAILURE_TIMEOUT = 60000L diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/BundleExtensions.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/BundleExtensions.kt new file mode 100644 index 0000000000..c96c28bb09 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/BundleExtensions.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.test.e2e.extension + +import android.os.Bundle + +fun Bundle.getRequiredArgument(argument: String): String { + return getString(argument) + ?: throw IllegalArgumentException("Missing required argument: $argument") +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/UiAutomatorExtensions.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/UiAutomatorExtensions.kt new file mode 100644 index 0000000000..5d96f4cc45 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/extension/UiAutomatorExtensions.kt @@ -0,0 +1,55 @@ +package net.mullvad.mullvadvpn.test.e2e.extension + +import androidx.test.uiautomator.By +import androidx.test.uiautomator.BySelector +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import java.util.regex.Pattern +import net.mullvad.mullvadvpn.test.e2e.constant.DEFAULT_INTERACTION_TIMEOUT + +fun UiDevice.findObjectByCaseInsensitiveText(text: String): UiObject2 { + return findObjectWithTimeout(By.text(Pattern.compile(text, Pattern.CASE_INSENSITIVE))) +} + +fun UiObject2.findObjectByCaseInsensitiveText(text: String): UiObject2 { + return findObjectWithTimeout(By.text(Pattern.compile(text, Pattern.CASE_INSENSITIVE))) +} + +fun UiDevice.findObjectWithTimeout( + selector: BySelector, + timeout: Long = DEFAULT_INTERACTION_TIMEOUT +): UiObject2 { + + wait( + Until.hasObject(selector), + timeout + ) + + return try { + findObject(selector) + } catch (e: NullPointerException) { + throw IllegalArgumentException( + "No matches for selector within timeout ($timeout): $selector" + ) + } +} + +fun UiObject2.findObjectWithTimeout( + selector: BySelector, + timeout: Long = DEFAULT_INTERACTION_TIMEOUT +): UiObject2 { + + wait( + Until.hasObject(selector), + timeout + ) + + return try { + findObject(selector) + } catch (e: NullPointerException) { + throw IllegalArgumentException( + "No matches for selector within timeout ($timeout): $selector" + ) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/AppInteractor.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/AppInteractor.kt new file mode 100644 index 0000000000..a6739b91e5 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/AppInteractor.kt @@ -0,0 +1,83 @@ +package net.mullvad.mullvadvpn.test.e2e.interactor + +import android.content.Context +import android.content.Intent +import android.widget.ImageButton +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import net.mullvad.mullvadvpn.test.e2e.constant.APP_LAUNCH_TIMEOUT +import net.mullvad.mullvadvpn.test.e2e.constant.CONNECTION_TIMEOUT +import net.mullvad.mullvadvpn.test.e2e.constant.LOGIN_TIMEOUT +import net.mullvad.mullvadvpn.test.e2e.constant.MULLVAD_PACKAGE +import net.mullvad.mullvadvpn.test.e2e.constant.SETTINGS_COG_ID +import net.mullvad.mullvadvpn.test.e2e.constant.TUNNEL_INFO_ID +import net.mullvad.mullvadvpn.test.e2e.constant.TUNNEL_OUT_ADDRESS_ID +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectWithTimeout + +class AppInteractor( + private val device: UiDevice, + private val targetContext: Context, + private val validTestAccountToken: String, + private val invalidTestAccountToken: String +) { + fun launch() { + device.pressHome() + // Wait for launcher + device.wait( + Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), + APP_LAUNCH_TIMEOUT + ) + val intent = + targetContext.packageManager.getLaunchIntentForPackage(MULLVAD_PACKAGE)?.apply { + // Clear out any previous instances + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + } + targetContext.startActivity(intent) + device.wait( + Until.hasObject(By.pkg(MULLVAD_PACKAGE).depth(0)), + APP_LAUNCH_TIMEOUT + ) + } + + fun launchAndEnsureLoggedIn(accountToken: String = validTestAccountToken) { + launch() + attemptLogin(accountToken) + ensureLoggedIn() + } + + fun attemptLogin(accountToken: String = validTestAccountToken) { + device.findObjectWithTimeout(By.text("Login")) + val loginObject = device.findObjectWithTimeout(By.clazz("android.widget.EditText")) + .apply { text = accountToken } + loginObject.parent.findObject(By.clazz(ImageButton::class.java)).click() + } + + fun ensureLoggedIn() { + device.findObjectWithTimeout(By.text("UNSECURED CONNECTION"), LOGIN_TIMEOUT) + } + + fun extractIpAddress(): String { + device.findObjectWithTimeout(By.res(TUNNEL_INFO_ID)).click() + return device.findObjectWithTimeout( + By.res(TUNNEL_OUT_ADDRESS_ID), + CONNECTION_TIMEOUT + ).text.extractIpAddress() + } + + fun clickSettingsCog() { + device.findObjectWithTimeout(By.res(SETTINGS_COG_ID)).click() + } + + fun clickListItemByText(text: String) { + device.findObjectWithTimeout(By.text(text)).click() + } + + fun clickActionButtonByText(text: String) { + device.findObjectWithTimeout(By.text(text)).click() + } + + private fun String.extractIpAddress(): String { + return split(" ")[1].split(" ")[0] + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/MullvadAccountInteractor.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/MullvadAccountInteractor.kt new file mode 100644 index 0000000000..8f3f55166f --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/MullvadAccountInteractor.kt @@ -0,0 +1,12 @@ +package net.mullvad.mullvadvpn.test.e2e.interactor + +import net.mullvad.mullvadvpn.test.e2e.misc.SimpleMullvadHttpClient + +class MullvadAccountInteractor( + private val httpClient: SimpleMullvadHttpClient, + private val testAccountToken: String +) { + fun cleanupAccount() { + httpClient.removeAllDevices(testAccountToken) + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/SystemSettingsInteractor.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/SystemSettingsInteractor.kt new file mode 100644 index 0000000000..bbe941d488 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/SystemSettingsInteractor.kt @@ -0,0 +1,36 @@ +package net.mullvad.mullvadvpn.test.e2e.interactor + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectByCaseInsensitiveText + +class SystemSettingsInteractor( + private val uiDevice: UiDevice, + private val context: Context +) { + fun openVpnSettings() { + val intent = Intent("com.intent.MAIN").apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + intent.component = ComponentName.unflattenFromString( + "com.android.settings/.Settings\$VpnSettingsActivity" + ) + context.startActivity(intent) + Thread.sleep(1000) + } + + fun removeAllVpnPermissions() { + openVpnSettings() + uiDevice.findObjects(By.descContains("Settings")).forEach { + it.click() + Thread.sleep(1000) + uiDevice.findObjectByCaseInsensitiveText("forget vpn").click() + Thread.sleep(1000) + uiDevice.findObjectByCaseInsensitiveText("forget").click() + } + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/WebViewInteractor.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/WebViewInteractor.kt new file mode 100644 index 0000000000..fd809b13dd --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/interactor/WebViewInteractor.kt @@ -0,0 +1,47 @@ +package net.mullvad.mullvadvpn.test.e2e.interactor + +import android.content.Context +import android.content.Intent +import android.view.View +import android.webkit.WebView +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import net.mullvad.mullvadvpn.TestActivity +import net.mullvad.mullvadvpn.test.e2e.constant.CONNECTION_CHECK_IS_CONNECTED +import net.mullvad.mullvadvpn.test.e2e.constant.CONN_CHECK_URL +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectByCaseInsensitiveText +import net.mullvad.mullvadvpn.test.e2e.extension.findObjectWithTimeout + +class WebViewInteractor( + private val context: Context, + private val device: UiDevice +) { + fun launchWebView(context: Context, url: String) { + val intent = Intent(context, TestActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + putExtra("url", url) + } + context.startActivity(intent) + } + + fun launchAndExtractConnCheckState(): ConnCheckState { + launchWebView(context, CONN_CHECK_URL) + val webView = device.findObjectWithTimeout(By.clazz(WebView::class.java)) + val stateText = device.findObjectByCaseInsensitiveText("using Mullvad VPN").apply { + click() + } + + // Wait for view to expand after click. + Thread.sleep(1000) + + val wireGuardIpv4ConnectionRow = webView.findObjects(By.clazz(View::class.java)) + .first { it.text?.endsWith("(WireGuard)") == true } + val wireGuardIpv4Address = wireGuardIpv4ConnectionRow.text.split(" ")[0].trim() + return ConnCheckState(stateText.text == CONNECTION_CHECK_IS_CONNECTED, wireGuardIpv4Address) + } + + data class ConnCheckState( + val isConnected: Boolean, + val ipAddress: String + ) +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CaptureScreenshotOnFailedTestRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CaptureScreenshotOnFailedTestRule.kt new file mode 100644 index 0000000000..5be65f7ec8 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CaptureScreenshotOnFailedTestRule.kt @@ -0,0 +1,31 @@ +package net.mullvad.mullvadvpn.test.e2e.misc + +import android.util.Log +import androidx.test.runner.screenshot.BasicScreenCaptureProcessor +import androidx.test.runner.screenshot.ScreenCaptureProcessor +import androidx.test.runner.screenshot.Screenshot +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import net.mullvad.mullvadvpn.test.e2e.constant.LOG_TAG +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +class CaptureScreenshotOnFailedTestRule : TestWatcher() { + override fun failed(e: Throwable?, description: Description?) { + Log.d(LOG_TAG, "Capturing screenshot of failed test: " + description?.methodName) + val timestamp = DateTimeFormatter.ISO_DATE_TIME.format(LocalDateTime.now()).replace(":", "") + val screenshotName = "$timestamp-${description?.methodName}" + captureScreenshot(screenshotName) + } + + private fun captureScreenshot(screenShotName: String) { + try { + val screenCapture = Screenshot.capture().apply { name = screenShotName } + val processorSet: MutableSet<ScreenCaptureProcessor> = HashSet() + processorSet.add(BasicScreenCaptureProcessor()) + screenCapture.process(processorSet) + } catch (ex: Exception) { + Log.d(LOG_TAG, "Error capturing screenshot: " + ex.message) + } + } +} diff --git a/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CleanupAccountTestRule.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CleanupAccountTestRule.kt new file mode 100644 index 0000000000..4ed5e72512 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/CleanupAccountTestRule.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.test.e2e.misc + +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry +import net.mullvad.mullvadvpn.test.e2e.constant.LOG_TAG +import net.mullvad.mullvadvpn.test.e2e.constant.VALID_TEST_ACCOUNT_TOKEN_ARGUMENT_KEY +import net.mullvad.mullvadvpn.test.e2e.extension.getRequiredArgument +import net.mullvad.mullvadvpn.test.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/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt new file mode 100644 index 0000000000..b06ecee780 --- /dev/null +++ b/android/test/e2e/src/main/kotlin/net/mullvad/mullvadvpn/test/e2e/misc/SimpleMullvadHttpClient.kt @@ -0,0 +1,191 @@ +package net.mullvad.mullvadvpn.test.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.test.e2e.BuildConfig +import net.mullvad.mullvadvpn.test.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." + } +} |
